#include "quakedef.h" #ifdef QUAKEHUD #define MAX_WEAPONS 64 //fixme: make dynamic. typedef enum { //one componant ff_death, ff_tkdeath, ff_suicide, ff_bonusfrag, ff_tkbonus, ff_flagtouch, ff_flagcaps, ff_flagdrops, //two componant ff_frags, //must be the first of the two componant ff_fragedby, ff_tkills, ff_tkilledby, } fragfilemsgtypes_t; typedef struct statmessage_s { fragfilemsgtypes_t type; int wid; size_t l1, l2; char *msgpart1; char *msgpart2; struct statmessage_s *next; } statmessage_t; typedef unsigned short stat; typedef struct { stat totaldeaths; stat totalsuicides; stat totalteamkills; stat totalkills; stat totaltouches; stat totalcaps; stat totaldrops; //I was going to keep track of kills with a certain gun - too much memory //track only your own and total weapon kills rather than per client struct wt_s { //these include you. stat kills; stat teamkills; stat suicides; stat ownkills; stat owndeaths; stat ownteamkills; stat ownteamdeaths; stat ownsuicides; char *fullname; char *abrev; char *image; char *codename; } weapontotals[MAX_WEAPONS]; struct ct_s { stat caps; //times they captured the flag stat drops; //times lost the flag stat grabs; //times grabbed flag stat owndeaths; //times you killed them stat ownkills; //times they killed you stat deaths; //times they died (including by you) stat kills; //times they killed (including by you) stat teamkills; //times they killed a team member. stat teamdeaths; //times they died to a team member. stat suicides; //times they were stupid. } clienttotals[MAX_CLIENTS]; qboolean readcaps; qboolean readkills; statmessage_t *message; } fragstats_t; cvar_t r_tracker_frags = CVARD("r_tracker_frags", "0", "0: like vanilla quake\n1: shows only your kills/deaths\n2: shows all kills\n"); static fragstats_t fragstats; int Stats_GetKills(int playernum) { return fragstats.clienttotals[playernum].kills; } int Stats_GetTKills(int playernum) { return fragstats.clienttotals[playernum].teamkills; } int Stats_GetDeaths(int playernum) { return fragstats.clienttotals[playernum].deaths; } int Stats_GetTouches(int playernum) { return fragstats.clienttotals[playernum].grabs; } int Stats_GetCaptures(int playernum) { return fragstats.clienttotals[playernum].caps; } qboolean Stats_HaveFlags(int showtype) { int i; if (showtype) return fragstats.readcaps; for (i = 0; i < cl.allocated_client_slots; i++) { if (fragstats.clienttotals[i].caps || fragstats.clienttotals[i].drops || fragstats.clienttotals[i].grabs) return fragstats.readcaps; } return false; } qboolean Stats_HaveKills(void) { return fragstats.readkills; } static char lastownfragplayer[64]; static float lastownfragtime; float Stats_GetLastOwnFrag(int seat, char *res, int reslen) { if (seat) { if (reslen) *res = 0; return 0; } //erk, realtime was reset? if (lastownfragtime > (float)realtime) lastownfragtime = 0; Q_strncpyz(res, lastownfragplayer, reslen); return realtime - lastownfragtime; }; static void Stats_OwnFrag(char *name) { Q_strncpyz(lastownfragplayer, name, sizeof(lastownfragplayer)); lastownfragtime = realtime; } void VARGS Stats_Message(char *msg, ...); qboolean Stats_TrackerImageLoaded(const char *in) { int error; if (in) return Font_TrackerValid(unicode_decode(&error, in, &in, true)); return false; } static char *Stats_GenTrackerImageString(char *in) { //images are of the form "foo \sg\ bar \q\" //which should eg be remapped to: "foo ^Ue200 bar foo ^Ue201" char res[256]; char image[MAX_QPATH]; char *outi; char *out; int i; if (!in || !*in) return NULL; for (out = res; *in && out < res+sizeof(res)-10; ) { if (*in == '\\') { in++; for (outi = image; *in && outi < image+sizeof(image)-10; ) { if (*in == '\\') break; *outi++ = *in++; } *outi = 0; in++; i = Font_RegisterTrackerImage(va("tracker/%s", image)); if (i) { char hexchars[16] = "0123456789abcdef"; *out++ = '^'; *out++ = 'U'; *out++ = hexchars[(i>>12)&15]; *out++ = hexchars[(i>>8)&15]; *out++ = hexchars[(i>>4)&15]; *out++ = hexchars[(i>>0)&15]; } else { //just copy the short name over, not much else we can do. for(outi = image; out < res+sizeof(res)-10 && *outi; ) *out++ = *outi++; } } else *out++ = *in++; } *out = 0; return Z_StrDup(res); } void Stats_FragMessage(int p1, int wid, int p2, qboolean teamkill) { static const char *nonplayers[] = { "BUG", "(teamkill)", "(suicide)", "(death)", "(unknown)", "(fixme)", "(fixme)" }; char message[512]; console_t *tracker; struct wt_s *w = &fragstats.weapontotals[wid]; const char *p1n = (p1 < 0)?nonplayers[-p1]:cl.players[p1].name; const char *p2n = (p2 < 0)?nonplayers[-p2]:cl.players[p2].name; int localplayer = (cl.playerview[0].cam_state == CAM_EYECAM)?cl.playerview[0].cam_spec_track:cl.playerview[0].playernum; #define YOU_GOOD S_COLOR_GREEN #define YOU_BAD S_COLOR_BLUE #define TEAM_GOOD S_COLOR_GREEN #define TEAM_BAD S_COLOR_RED #define TEAM_VBAD S_COLOR_BLUE #define TEAM_NEUTRAL S_COLOR_WHITE //enemy team thing that does not directly affect us #define ENEMY_GOOD S_COLOR_RED #define ENEMY_BAD S_COLOR_GREEN #define ENEMY_NEUTRAL S_COLOR_WHITE char *p1c = S_COLOR_WHITE; char *p2c = S_COLOR_WHITE; if (!r_tracker_frags.ival) return; if (r_tracker_frags.ival < 2) if (p1 != localplayer && p2 != localplayer) return; if (teamkill) {//team kills/suicides are always considered bad. if (p1 == localplayer) p1c = YOU_BAD; else if (cl.teamplay && !strcmp(cl.players[p1].team, cl.players[localplayer].team)) p1c = TEAM_VBAD; else p1c = TEAM_NEUTRAL; p2c = p1c; } else if (p1 == p2) p1c = p2c = YOU_BAD; else if (cl.teamplay && p1 >= 0 && p2 >= 0 && !strcmp(cl.players[p1].team, cl.players[p2].team)) p1c = p2c = TEAM_VBAD; else { if (p2 >= 0) { //us/teammate killing is good - unless it was a teammate. if (p2 == localplayer) p2c = YOU_GOOD; else if (cl.teamplay && !strcmp(cl.players[p2].team, cl.players[localplayer].team)) p2c = TEAM_GOOD; else p2c = ENEMY_GOOD; } if (p1 >= 0) { //us/teammate dying is bad. if (p1 == localplayer) p1c = YOU_BAD; else if (cl.teamplay && !strcmp(cl.players[p1].team, cl.players[localplayer].team)) p1c = TEAM_BAD; else p1c = p2c; } } Q_snprintfz(message, sizeof(message), "%s%s ^7%s %s%s\n", p2c, p2n, Stats_TrackerImageLoaded(w->image)?w->image:w->abrev, p1c, p1n); tracker = Con_FindConsole("tracker"); if (!tracker) { tracker = Con_Create("tracker", CONF_HIDDEN|CONF_NOTIFY|CONF_NOTIFY_RIGHT|CONF_NOTIFY_BOTTOM); //this stuff should be configurable tracker->notif_l = tracker->maxlines = 8; tracker->notif_x = 0.5; tracker->notif_y = 0.333; tracker->notif_w = 1-tracker->notif_x; tracker->notif_t = 4; tracker->notif_fade = 1; } Con_PrintCon(tracker, message, tracker->parseflags); } void Stats_Evaluate(fragfilemsgtypes_t mt, int wid, int p1, int p2) { qboolean u1; qboolean u2; if (mt == ff_frags || mt == ff_tkills) { int tmp = p1; p1 = p2; p2 = tmp; } u1 = (p1 == (cl.playerview[0].playernum)); u2 = (p2 == (cl.playerview[0].playernum)); if (p1 == -1) p1 = p2; if (p2 == -1) p2 = p1; //messages are killed weapon killer switch(mt) { case ff_death: if (u1) { fragstats.weapontotals[wid].owndeaths++; fragstats.weapontotals[wid].ownkills++; } fragstats.weapontotals[wid].kills++; if (p1 >= 0) fragstats.clienttotals[p1].deaths++; fragstats.totaldeaths++; Stats_FragMessage(p1, wid, -3, true); if (u1) Stats_Message("You died\n%s deaths: %i\n", fragstats.weapontotals[wid].fullname, fragstats.weapontotals[wid].owndeaths); break; case ff_suicide: if (u1) { fragstats.weapontotals[wid].ownsuicides++; fragstats.weapontotals[wid].owndeaths++; fragstats.weapontotals[wid].ownkills++; } fragstats.weapontotals[wid].suicides++; fragstats.weapontotals[wid].kills++; if (p1 >= 0) { fragstats.clienttotals[p1].suicides++; fragstats.clienttotals[p1].deaths++; } fragstats.totalsuicides++; fragstats.totaldeaths++; Stats_FragMessage(p1, wid, -2, true); if (u1) Stats_Message("You killed your own dumb self\n%s suicides: %i (%i)\n", fragstats.weapontotals[wid].fullname, fragstats.weapontotals[wid].ownsuicides, fragstats.weapontotals[wid].suicides); break; case ff_bonusfrag: if (u1) fragstats.weapontotals[wid].ownkills++; fragstats.weapontotals[wid].kills++; if (p1 >= 0) fragstats.clienttotals[p1].kills++; fragstats.totalkills++; Stats_FragMessage(-4, wid, p1, false); if (u1) { Stats_OwnFrag("someone"); Stats_Message("You killed someone\n%s kills: %i\n", fragstats.weapontotals[wid].fullname, fragstats.weapontotals[wid].ownkills); } break; case ff_tkbonus: if (u1) fragstats.weapontotals[wid].ownkills++; fragstats.weapontotals[wid].kills++; fragstats.clienttotals[p1].kills++; fragstats.totalkills++; if (u1) fragstats.weapontotals[wid].ownteamkills++; fragstats.weapontotals[wid].teamkills++; if (p1 >= 0) fragstats.clienttotals[p1].teamkills++; fragstats.totalteamkills++; Stats_FragMessage(-1, wid, p1, true); if (u1) { Stats_Message("You killed your teammate\n%s teamkills: %i\n", fragstats.weapontotals[wid].fullname, fragstats.weapontotals[wid].ownteamkills); } break; case ff_flagtouch: fragstats.clienttotals[p1].grabs++; fragstats.totaltouches++; if (u1 && p1 >= 0) { Stats_Message("You grabbed the flag\nflag grabs: %i (%i)\n", fragstats.clienttotals[p1].grabs, fragstats.totaltouches); } break; case ff_flagcaps: if (p1 >= 0) fragstats.clienttotals[p1].caps++; fragstats.totalcaps++; if (u1 && p1 >= 0) { Stats_Message("You captured the flag\nflag captures: %i (%i)\n", fragstats.clienttotals[p1].caps, fragstats.totalcaps); } break; case ff_flagdrops: if (p1 >= 0) fragstats.clienttotals[p1].drops++; fragstats.totaldrops++; if (u1 && p1 >= 0) { Stats_Message("You dropped the flag\nflag drops: %i (%i)\n", fragstats.clienttotals[p1].drops, fragstats.totaldrops); } break; //p1 died, p2 killed case ff_frags: case ff_fragedby: fragstats.weapontotals[wid].kills++; if (p1 >= 0) fragstats.clienttotals[p1].deaths++; fragstats.totaldeaths++; if (u1) { fragstats.weapontotals[wid].owndeaths++; if (p1 >= 0 && p2 >= 0) Stats_Message("%s killed you\n%s deaths: %i (%i/%i)\n", cl.players[p2].name, fragstats.weapontotals[wid].fullname, fragstats.clienttotals[p2].owndeaths, fragstats.weapontotals[wid].owndeaths, fragstats.totaldeaths); } if (p2 >= 0) fragstats.clienttotals[p2].kills++; fragstats.totalkills++; if (u2) { if (p1 >= 0) Stats_OwnFrag(cl.players[p1].name); fragstats.weapontotals[wid].ownkills++; if (p1 >= 0 && p2 >= 0) Stats_Message("You killed %s\n%s kills: %i (%i/%i)\n", cl.players[p1].name, fragstats.weapontotals[wid].fullname, fragstats.clienttotals[p2].kills, fragstats.weapontotals[wid].kills, fragstats.totalkills); } Stats_FragMessage(p1, wid, p2, false); break; case ff_tkdeath: //killed by a teammate, but we don't know who //kinda useless, but this is all some mods give us fragstats.weapontotals[wid].teamkills++; fragstats.weapontotals[wid].kills++; fragstats.totalkills++; //its a kill, but we don't know who from fragstats.totalteamkills++; if (u1) fragstats.weapontotals[wid].owndeaths++; fragstats.clienttotals[p1].teamdeaths++; fragstats.clienttotals[p1].deaths++; fragstats.totaldeaths++; Stats_FragMessage(p1, wid, -1, true); if (u1) Stats_Message("Your teammate killed you\n%s deaths: %i\n", fragstats.weapontotals[wid].fullname, fragstats.weapontotals[wid].owndeaths); break; case ff_tkills: case ff_tkilledby: //p1 killed by p2 (kills is already inverted) fragstats.weapontotals[wid].teamkills++; fragstats.weapontotals[wid].kills++; if (u1) { fragstats.weapontotals[wid].ownteamdeaths++; fragstats.weapontotals[wid].owndeaths++; } if (p1 >= 0) { fragstats.clienttotals[p1].teamdeaths++; fragstats.clienttotals[p1].deaths++; } fragstats.totaldeaths++; if (u2) { fragstats.weapontotals[wid].ownkills++; fragstats.weapontotals[wid].ownkills++; } if (p2 >= 0) { fragstats.clienttotals[p2].teamkills++; fragstats.clienttotals[p2].kills++; } fragstats.totalkills++; fragstats.totalteamkills++; Stats_FragMessage(p1, wid, p2, false); if (u1 && p2 >= 0) Stats_Message("%s killed you\n%s deaths: %i (%i/%i)\n", cl.players[p2].name, fragstats.weapontotals[wid].fullname, fragstats.clienttotals[p2].owndeaths, fragstats.weapontotals[wid].owndeaths, fragstats.totaldeaths); if (u2 && p1 >= 0 && p2 >= 0) { Stats_OwnFrag(cl.players[p1].name); Stats_Message("You killed %s\n%s kills: %i (%i/%i)\n", cl.players[p1].name, fragstats.weapontotals[wid].fullname, fragstats.clienttotals[p2].kills, fragstats.weapontotals[wid].kills, fragstats.totalkills); } break; } } static int Stats_FindWeapon(char *codename, qboolean create) { int i; if (!strcmp(codename, "NONE")) return 0; if (!strcmp(codename, "NULL")) return 0; if (!strcmp(codename, "NOWEAPON")) return 0; for (i = 1; i < MAX_WEAPONS; i++) { if (!fragstats.weapontotals[i].codename) { fragstats.weapontotals[i].codename = Z_Malloc(strlen(codename)+1); strcpy(fragstats.weapontotals[i].codename, codename); return i; } if (!stricmp(fragstats.weapontotals[i].codename, codename)) { if (create) return -2; return i; } } return -1; } static void Stats_StatMessage(fragfilemsgtypes_t type, int wid, char *token1, char *token2) { statmessage_t *ms; char *t; ms = Z_Malloc(sizeof(statmessage_t) + strlen(token1)+1 + (token2 && *token2?strlen(token2)+1:0)); t = (char *)(ms+1); ms->msgpart1 = t; strcpy(t, token1); ms->l1 = strlen(ms->msgpart1); if (token2 && *token2) { t += strlen(t)+1; ms->msgpart2 = t; strcpy(t, token2); ms->l2 = strlen(ms->msgpart2); } ms->type = type; ms->wid = wid; ms->next = fragstats.message; fragstats.message = ms; //we have a message type, save the fact that we have it. if (type == ff_flagtouch || type == ff_flagcaps || type == ff_flagdrops) fragstats.readcaps = true; if (type == ff_frags || type == ff_fragedby) fragstats.readkills = true; } void Stats_Clear(void) { int i; statmessage_t *ms; while (fragstats.message) { ms = fragstats.message; fragstats.message = ms->next; Z_Free(ms); } for (i = 1; i < MAX_WEAPONS; i++) { if (fragstats.weapontotals[i].codename) Z_Free(fragstats.weapontotals[i].codename); if (fragstats.weapontotals[i].fullname) Z_Free(fragstats.weapontotals[i].fullname); if (fragstats.weapontotals[i].abrev) Z_Free(fragstats.weapontotals[i].abrev); if (fragstats.weapontotals[i].image) Z_Free(fragstats.weapontotals[i].image); } memset(&fragstats, 0, sizeof(fragstats)); } #define Z_Copy(tk) tz = Z_Malloc(strlen(tk)+1);strcpy(tz, tk) //remember the braces void Stats_Init(void) { Cvar_Register(&r_tracker_frags, NULL); } static void Stats_LoadFragFile(char *name) { char filename[MAX_QPATH]; char *file; char *end; char *tk, *tz; char oend; Stats_Clear(); strcpy(filename, name); COM_DefaultExtension(filename, ".dat", sizeof(filename)); file = COM_LoadTempFile(filename, 0, NULL); if (!file || !*file) { Con_DPrintf("Couldn't load %s\n", filename); return; } else Con_DPrintf("Loaded %s\n", filename); oend = 1; for (;*file;) { if (!oend) break; for (end = file; *end && *end != '\n'; end++) ; oend = *end; *end = '\0'; Cmd_TokenizeString(file, true, false); file = end+1; if (!Cmd_Argc()) continue; tk = Cmd_Argv(0); if (!stricmp(tk, "#fragfile")) { tk = Cmd_Argv(1); if (!stricmp(tk, "version")) {} else if (!stricmp(tk, "gamedir")) {} else Con_Printf("Unrecognised #meta \"%s\"\n", tk); } else if (!stricmp(tk, "#meta")) { tk = Cmd_Argv(1); if (!stricmp(tk, "title")) {} else if (!stricmp(tk, "description")) {} else if (!stricmp(tk, "author")) {} else if (!stricmp(tk, "email")) {} else if (!stricmp(tk, "webpage")) {} else {Con_Printf("Unrecognised #meta \"%s\"\n", tk);continue;} } else if (!stricmp(tk, "#define")) { tk = Cmd_Argv(1); if (!stricmp(tk, "weapon_class") || !stricmp(tk, "wc")) { int wid; tk = Cmd_Argv(2); wid = Stats_FindWeapon(tk, true); if (wid == -1) {Con_Printf("Too many weapon definitions. The max is %i\n", MAX_WEAPONS);continue;} else if (wid < -1) {Con_Printf("Weapon \"%s\" is already defined\n", tk);continue;} else { fragstats.weapontotals[wid].fullname = Z_Copy(Cmd_Argv(3)); fragstats.weapontotals[wid].abrev = Z_Copy(Cmd_Argv(4)); fragstats.weapontotals[wid].image = Stats_GenTrackerImageString(Cmd_Argv(5)); } } else if (!stricmp(tk, "obituary") || !stricmp(tk, "obit")) { int fftype; tk = Cmd_Argv(2); if (!stricmp(tk, "PLAYER_DEATH")) {fftype = ff_death;} else if (!stricmp(tk, "PLAYER_SUICIDE")) {fftype = ff_suicide;} else if (!stricmp(tk, "X_FRAGS_UNKNOWN")) {fftype = ff_bonusfrag;} else if (!stricmp(tk, "X_TEAMKILLS_UNKNOWN")) {fftype = ff_tkbonus;} else if (!stricmp(tk, "X_TEAMKILLED_UNKNOWN")) {fftype = ff_tkdeath;} else if (!stricmp(tk, "X_FRAGS_Y")) {fftype = ff_frags;} else if (!stricmp(tk, "X_FRAGGED_BY_Y")) {fftype = ff_fragedby;} else if (!stricmp(tk, "X_TEAMKILLS_Y")) {fftype = ff_tkills;} else if (!stricmp(tk, "X_TEAMKILLED_BY_Y")) {fftype = ff_tkilledby;} else {Con_Printf("Unrecognised obituary \"%s\"\n", tk);continue;} Stats_StatMessage(fftype, Stats_FindWeapon(Cmd_Argv(3), false), Cmd_Argv(4), Cmd_Argv(5)); } else if (!stricmp(tk, "flag_alert") || !stricmp(tk, "flag_msg")) { int fftype; tk = Cmd_Argv(2); if (!stricmp(tk, "X_TOUCHES_FLAG")) {fftype = ff_flagtouch;} else if (!stricmp(tk, "X_GETS_FLAG")) {fftype = ff_flagtouch;} else if (!stricmp(tk, "X_TAKES_FLAG")) {fftype = ff_flagtouch;} else if (!stricmp(tk, "X_CAPTURES_FLAG")) {fftype = ff_flagcaps;} else if (!stricmp(tk, "X_CAPS_FLAG")) {fftype = ff_flagcaps;} else if (!stricmp(tk, "X_SCORES")) {fftype = ff_flagcaps;} else if (!stricmp(tk, "X_DROPS_FLAG")) {fftype = ff_flagdrops;} else if (!stricmp(tk, "X_FUMBLES_FLAG")) {fftype = ff_flagdrops;} else if (!stricmp(tk, "X_LOSES_FLAG")) {fftype = ff_flagdrops;} else {Con_Printf("Unrecognised flag alert \"%s\"\n", tk);continue;} Stats_StatMessage(fftype, 0, Cmd_Argv(3), NULL); } else {Con_Printf("Unrecognised directive \"%s\"\n", tk);continue;} } else {Con_Printf("Unrecognised directive \"%s\"\n", tk);continue;} } } static int qm_strcmp(const char *s1, const char *s2)//not like strcmp at all... { while(*s1) { if ((*s1++&0x7f)!=(*s2++&0x7f)) return 1; } return 0; } /* static int qm_stricmp(char *s1, char *s2)//not like strcmp at all... { int c1,c2; while(*s1) { c1 = *s1++&0x7f; c2 = *s2++&0x7f; if (c1 >= 'A' && c1 <= 'Z') c1 = c1 - 'A' + 'a'; if (c2 >= 'A' && c2 <= 'Z') c2 = c2 - 'A' + 'a'; if (c1!=c2) return 1; } return 0; } */ static int Stats_ExtractName(const char **line) { int i; int bm; int ml = 0; int l; bm = -1; for (i = 0; i < cl.allocated_client_slots; i++) { if (!qm_strcmp(cl.players[i].name, *line)) { l = strlen(cl.players[i].name); if (l > ml) { bm = i; ml = l; } } } *line += ml; return bm; } qboolean Stats_ParsePickups(const char *line) { #ifdef HAVE_LEGACY //fixme: rework this to support custom strings, with custom pickup icons if (!Q_strncmp(line, "You got the ", 12)) //weapons, ammo, keys, powerups return true; if (!Q_strncmp(line, "You got armor", 13)) //caaake... return true; if (!Q_strncmp(line, "You get ", 8)) //backpacks return true; if (!Q_strncmp(line, "You receive ", 12)) //%i health\n return true; #endif return false; } qboolean Stats_ParsePrintLine(const char *line) { statmessage_t *ms; int p1; int p2; const char *m2; p1 = Stats_ExtractName(&line); if (p1<0) //reject it. { return false; } for (ms = fragstats.message; ms; ms = ms->next) { if (!Q_strncmp(ms->msgpart1, line, ms->l1)) { if (ms->type >= ff_frags) { //two players m2 = line + ms->l1; p2 = Stats_ExtractName(&m2); if ((!ms->msgpart2 && *m2=='\n') || (ms->msgpart2 && !Q_strncmp(ms->msgpart2, m2, ms->l2))) { Stats_Evaluate(ms->type, ms->wid, p1, p2); return true; //done. } } else { //one player Stats_Evaluate(ms->type, ms->wid, p1, p1); return true; //done. } } } return false; } void Stats_NewMap(void) { Stats_LoadFragFile("fragfile"); } #endif