445 lines
15 KiB
C
445 lines
15 KiB
C
/*
|
|
* static char *rcsid_hiscore_c =
|
|
* "$Id: hiscore.c 11578 2009-02-23 22:02:27Z lalo $";
|
|
*/
|
|
|
|
/*
|
|
CrossFire, A Multiplayer game for X-windows
|
|
|
|
Copyright (C) 2006 Mark Wedel & Crossfire Development Team
|
|
Copyright (C) 1992 Frank Tore Johansen
|
|
|
|
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., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
The authors can be reached via e-mail at crossfire-devel@real-time.com
|
|
*/
|
|
|
|
/**
|
|
* @file
|
|
* Hiscore handling functions.
|
|
*/
|
|
|
|
#include <global.h>
|
|
#ifndef __CEXTRACT__
|
|
#include <sproto.h>
|
|
#endif
|
|
|
|
/**
|
|
* The score structure is used when treating new high-scores.
|
|
*/
|
|
|
|
typedef struct scr {
|
|
char name[BIG_NAME]; /**< Name. */
|
|
char title[BIG_NAME]; /**< Title. */
|
|
char killer[BIG_NAME]; /**< Name (+ title) or "left". */
|
|
sint64 exp; /**< Experience. */
|
|
char maplevel[BIG_NAME]; /**< Killed on what level. */
|
|
int maxhp, maxsp, maxgrace; /**< Max hp, sp, grace when killed. */
|
|
int position; /**< Position in the highscore list. */
|
|
} score;
|
|
|
|
/**
|
|
* Spool works mostly like strtok(char *, ":"), but it can also
|
|
* log a specified error message if something goes wrong.
|
|
*
|
|
* @param bp
|
|
* string to search into. Should be non NULL for first call, NULL for subsequent calls.
|
|
* @param error
|
|
* message to display in case there is an error.
|
|
* @return
|
|
* next token, NULL if no more found.
|
|
* @todo make thread-safe. is this function really useful?
|
|
*/
|
|
static char *spool(char *bp, const char *error) {
|
|
static char *prev_pos = NULL;
|
|
char *next_pos;
|
|
|
|
if (bp == NULL) {
|
|
if (prev_pos == NULL) {
|
|
LOG(llevError, "Called spool (%s) with NULL without previous call.\n", error);
|
|
return NULL;
|
|
}
|
|
bp = prev_pos;
|
|
}
|
|
if (*bp == '\0') {
|
|
LOG(llevError, "spool: End of line at %s\n", error);
|
|
return NULL;
|
|
}
|
|
if ((next_pos = strchr(bp, ':')) != NULL) {
|
|
*next_pos = '\0';
|
|
prev_pos = next_pos+1;
|
|
} else
|
|
prev_pos = NULL;
|
|
return bp;
|
|
}
|
|
|
|
/**
|
|
* Does what it says, copies the contents of the first score structure
|
|
* to the second one.
|
|
*
|
|
* @param sc1
|
|
* what to copy.
|
|
* @param sc2
|
|
* where to copy to.
|
|
*/
|
|
static void copy_score(const score *sc1, score *sc2) {
|
|
strncpy(sc2->name, sc1->name, BIG_NAME);
|
|
sc2->name[BIG_NAME-1] = '\0';
|
|
strncpy(sc2->title, sc1->title, BIG_NAME);
|
|
sc2->title[BIG_NAME-1] = '\0';
|
|
strncpy(sc2->killer, sc1->killer, BIG_NAME);
|
|
sc2->killer[BIG_NAME-1] = '\0';
|
|
sc2->exp = sc1->exp;
|
|
strcpy(sc2->maplevel, sc1->maplevel);
|
|
sc2->maxhp = sc1->maxhp;
|
|
sc2->maxsp = sc1->maxsp;
|
|
sc2->maxgrace = sc1->maxgrace;
|
|
}
|
|
|
|
/**
|
|
* Writes the given score structure to specified buffer.
|
|
*
|
|
* @param sc
|
|
* score to write.
|
|
* @param buf
|
|
* buffer to write to.
|
|
* @param size
|
|
* buf's size.
|
|
*/
|
|
static void put_score(const score *sc, char *buf, int size) {
|
|
snprintf(buf, size, "%s:%s:%"FMT64":%s:%s:%d:%d:%d", sc->name, sc->title, sc->exp, sc->killer, sc->maplevel, sc->maxhp, sc->maxsp, sc->maxgrace);
|
|
}
|
|
|
|
/**
|
|
* The opposite of put_score(), get_score reads from the given buffer into
|
|
* a static score structure, and returns a pointer to it.
|
|
*
|
|
* @param bp
|
|
* string to parse.
|
|
* @return
|
|
* parsed score.
|
|
* @todo make thread-safe, remove static stuff.
|
|
*/
|
|
|
|
static score *get_score(char *bp) {
|
|
static score sc;
|
|
char *cp;
|
|
|
|
if ((cp = strchr(bp, '\n')) != NULL)
|
|
*cp = '\0';
|
|
|
|
if ((cp = spool(bp, "name")) == NULL)
|
|
return NULL;
|
|
strncpy(sc.name, cp, BIG_NAME);
|
|
sc.name[BIG_NAME-1] = '\0';
|
|
|
|
if ((cp = spool(NULL, "title")) == NULL)
|
|
return NULL;
|
|
strncpy(sc.title, cp, BIG_NAME);
|
|
sc.title[BIG_NAME-1] = '\0';
|
|
|
|
if ((cp = spool(NULL, "score")) == NULL)
|
|
return NULL;
|
|
|
|
sscanf(cp, "%"FMT64, &sc.exp);
|
|
|
|
if ((cp = spool(NULL, "killer")) == NULL)
|
|
return NULL;
|
|
strncpy(sc.killer, cp, BIG_NAME);
|
|
sc.killer[BIG_NAME-1] = '\0';
|
|
|
|
if ((cp = spool(NULL, "map")) == NULL)
|
|
return NULL;
|
|
strncpy(sc.maplevel, cp, BIG_NAME);
|
|
sc.maplevel[BIG_NAME-1] = '\0';
|
|
|
|
if ((cp = spool(NULL, "maxhp")) == NULL)
|
|
return NULL;
|
|
sscanf(cp, "%d", &sc.maxhp);
|
|
|
|
if ((cp = spool(NULL, "maxsp")) == NULL)
|
|
return NULL;
|
|
sscanf(cp, "%d", &sc.maxsp);
|
|
|
|
if ((cp = spool(NULL, "maxgrace")) == NULL)
|
|
return NULL;
|
|
sscanf(cp, "%d", &sc.maxgrace);
|
|
return ≻
|
|
}
|
|
|
|
/**
|
|
* Formats one score to display to a player.
|
|
*
|
|
* @param sc
|
|
* score to format.
|
|
* @param buf
|
|
* buffer to write to. Will contain suitably formatted score.
|
|
* @param size
|
|
* length of buf.
|
|
* @return
|
|
* buf.
|
|
*/
|
|
static char *draw_one_high_score(const score *sc, char *buf, int size) {
|
|
if (!strncmp(sc->killer, "quit", MAX_NAME))
|
|
snprintf(buf, size, "[Fixed]%3d %10"FMT64"[Print] %s the %s quit the game on map %s <%d><%d><%d>.",
|
|
sc->position, sc->exp, sc->name, sc->title, sc->maplevel, sc->maxhp, sc->maxsp, sc->maxgrace);
|
|
else if (!strncmp(sc->killer, "left", MAX_NAME))
|
|
snprintf(buf, size, "[Fixed]%3d %10"FMT64"[Print] %s the %s left the game on map %s <%d><%d><%d>.",
|
|
sc->position, sc->exp, sc->name, sc->title, sc->maplevel, sc->maxhp, sc->maxsp, sc->maxgrace);
|
|
else
|
|
snprintf(buf, size, "[Fixed]%3d %10"FMT64"[Print] %s the %s was killed by %s on map %s <%d><%d><%d>.",
|
|
sc->position, sc->exp, sc->name, sc->title, sc->killer, sc->maplevel, sc->maxhp, sc->maxsp, sc->maxgrace);
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Adds the given score-structure to the high-score list, but
|
|
* only if it was good enough to deserve a place.
|
|
*
|
|
* @param new_score
|
|
* score to add.
|
|
* @return
|
|
* old player score.
|
|
* @todo remove static buffer.
|
|
*/
|
|
static score *add_score(score *new_score) {
|
|
FILE *fp;
|
|
static score old_score;
|
|
score *tmp_score, pscore[HIGHSCORE_LENGTH];
|
|
char buf[MAX_BUF], filename[MAX_BUF], bp[MAX_BUF];
|
|
int nrofscores = 0, flag = 0, i, comp;
|
|
|
|
new_score->position = HIGHSCORE_LENGTH+1;
|
|
old_score.position = -1;
|
|
snprintf(filename, sizeof(filename), "%s/%s", settings.localdir, HIGHSCORE);
|
|
if ((fp = open_and_uncompress(filename, 1, &comp)) != NULL) {
|
|
while (fgets(buf, MAX_BUF, fp) != NULL && nrofscores < HIGHSCORE_LENGTH) {
|
|
if ((tmp_score = get_score(buf)) == NULL)
|
|
break;
|
|
if (!flag && new_score->exp >= tmp_score->exp) {
|
|
copy_score(new_score, &pscore[nrofscores]);
|
|
new_score->position = nrofscores;
|
|
flag = 1;
|
|
if (++nrofscores >= HIGHSCORE_LENGTH)
|
|
break;
|
|
}
|
|
if (!strcmp(new_score->name, tmp_score->name)) { /* Another entry */
|
|
copy_score(tmp_score, &old_score);
|
|
old_score.position = nrofscores;
|
|
if (flag)
|
|
continue;
|
|
}
|
|
copy_score(tmp_score, &pscore[nrofscores++]);
|
|
}
|
|
close_and_delete(fp, comp);
|
|
}
|
|
if (old_score.position != -1 && old_score.exp >= new_score->exp)
|
|
return &old_score; /* Did not beat old score */
|
|
if (!flag && nrofscores < HIGHSCORE_LENGTH)
|
|
copy_score(new_score, &pscore[nrofscores++]);
|
|
if ((fp = fopen(filename, "w")) == NULL) {
|
|
LOG(llevError, "Cannot write to highscore file %s: %s\n", filename, strerror_local(errno, buf, sizeof(buf)));
|
|
return NULL;
|
|
}
|
|
for (i = 0; i < nrofscores; i++) {
|
|
put_score(&pscore[i], bp, sizeof(bp));
|
|
fprintf(fp, "%s\n", bp);
|
|
}
|
|
fclose(fp);
|
|
if (flag) {
|
|
/* Eneq(@csd.uu.se): Patch to fix error in adding a new score to the
|
|
hiscore-list */
|
|
if (old_score.position == -1)
|
|
return new_score;
|
|
return &old_score;
|
|
}
|
|
new_score->position = -1;
|
|
if (old_score.position != -1)
|
|
return &old_score;
|
|
if (nrofscores) {
|
|
copy_score(&pscore[nrofscores-1], &old_score);
|
|
return &old_score;
|
|
}
|
|
LOG(llevError, "Highscore error.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Checks if player should enter the hiscore, and if so writes her into the list.
|
|
*
|
|
* @param op
|
|
* player to check.
|
|
* @param quiet
|
|
* If set, don't print anything out - used for periodic updates during game
|
|
* play or when player unexpected quits - don't need to print anything
|
|
* in those cases
|
|
*/
|
|
void check_score(object *op, int quiet) {
|
|
score new_score;
|
|
score *old_score;
|
|
char bufscore[MAX_BUF];
|
|
|
|
if (op->stats.exp == 0)
|
|
return;
|
|
|
|
if (!op->contr->name_changed) {
|
|
if (op->stats.exp > 0) {
|
|
if (!quiet)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
|
|
"As you haven't changed your name, you won't "
|
|
"get into the high-score list.", NULL);
|
|
}
|
|
return;
|
|
}
|
|
if (QUERY_FLAG(op, FLAG_WAS_WIZ)) {
|
|
if (!quiet)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
|
|
"Since you have been in wizard mode, "
|
|
"you can't enter the high-score list.", NULL);
|
|
return;
|
|
}
|
|
if (op->contr->explore) {
|
|
if (!quiet)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
|
|
"Since you were in explore mode, "
|
|
"you can't enter the high-score list.", NULL);
|
|
return;
|
|
}
|
|
if (!op->stats.exp) {
|
|
if (!quiet)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_APPLY, MSG_TYPE_APPLY_ERROR,
|
|
"You don't deserve to save your character yet.", NULL);
|
|
return;
|
|
}
|
|
|
|
strncpy(new_score.name, op->name, BIG_NAME);
|
|
new_score.name[BIG_NAME-1] = '\0';
|
|
strncpy(new_score.title, op->contr->own_title, BIG_NAME);
|
|
if (new_score.title[0] == '\0')
|
|
strncpy(new_score.title, op->contr->title, BIG_NAME);
|
|
new_score.title[BIG_NAME-1] = '\0';
|
|
strncpy(new_score.killer, op->contr->killer, BIG_NAME);
|
|
if (new_score.killer[0] == '\0')
|
|
strcpy(new_score.killer, "a dungeon collapse");
|
|
new_score.killer[BIG_NAME-1] = '\0';
|
|
new_score.exp = op->stats.exp;
|
|
if (op->map == NULL)
|
|
*new_score.maplevel = '\0';
|
|
else {
|
|
strncpy(new_score.maplevel, op->map->name ? op->map->name : op->map->path, BIG_NAME-1);
|
|
new_score.maplevel[BIG_NAME-1] = '\0';
|
|
}
|
|
new_score.maxhp = (int)op->stats.maxhp;
|
|
new_score.maxsp = (int)op->stats.maxsp;
|
|
new_score.maxgrace = (int)op->stats.maxgrace;
|
|
if ((old_score = add_score(&new_score)) == NULL) {
|
|
if (!quiet)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
|
|
"Error in the highscore list.", NULL);
|
|
return;
|
|
}
|
|
/* Everything below here is just related to print messages
|
|
* to the player. If quiet is set, we can just return
|
|
* now.
|
|
*/
|
|
if (quiet)
|
|
return;
|
|
|
|
if (new_score.position == -1) {
|
|
new_score.position = HIGHSCORE_LENGTH+1; /* Not strictly correct... */
|
|
|
|
if (!strcmp(old_score->name, new_score.name))
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
"You didn't beat your last highscore:", NULL);
|
|
else
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
"You didn't enter the highscore list:", NULL);
|
|
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
draw_one_high_score(old_score, bufscore, sizeof(bufscore)), NULL);
|
|
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
draw_one_high_score(&new_score, bufscore, sizeof(bufscore)), NULL);
|
|
return;
|
|
}
|
|
if (old_score->exp >= new_score.exp)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
"You didn't beat your last score:", NULL);
|
|
else
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
"You beat your last score:", NULL);
|
|
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
draw_one_high_score(old_score, bufscore, sizeof(bufscore)), NULL);
|
|
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
draw_one_high_score(&new_score, bufscore, sizeof(bufscore)), NULL);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Displays the high score file.
|
|
*
|
|
* @param op
|
|
* player asking for the score file.
|
|
* @param max
|
|
* maximum number of scores to display.
|
|
* @param match
|
|
* if set, will only print players with name or title containing the string (non case-sensitive).
|
|
*/
|
|
void display_high_score(object *op, int max, const char *match) {
|
|
FILE *fp;
|
|
char buf[MAX_BUF], scorebuf[MAX_BUF];
|
|
int i = 0, j = 0, comp;
|
|
score *sc;
|
|
|
|
snprintf(buf, sizeof(buf), "%s/%s", settings.localdir, HIGHSCORE);
|
|
if ((fp = open_and_uncompress(buf, 0, &comp)) == NULL) {
|
|
char err[MAX_BUF];
|
|
|
|
LOG(llevError, "Cannot open highscore file %s: %s\n", buf, strerror_local(errno, err, sizeof(err)));
|
|
if (op != NULL)
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
|
|
"There is no highscore file.", NULL);
|
|
return;
|
|
}
|
|
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE,
|
|
"[Fixed]Nr Score Who <max hp><max sp><max grace>",
|
|
"Nr Score Who <max hp><max sp><max grace>");
|
|
|
|
while (fgets(buf, MAX_BUF, fp) != NULL) {
|
|
if (j >= HIGHSCORE_LENGTH || i >= (max-1))
|
|
break;
|
|
if ((sc = get_score(buf)) == NULL)
|
|
break;
|
|
sc->position = ++j;
|
|
if (match == NULL
|
|
|| strcasestr_local(sc->name, match)
|
|
|| strcasestr_local(sc->title, match)) {
|
|
draw_one_high_score(sc, scorebuf, sizeof(scorebuf));
|
|
i++;
|
|
} else
|
|
continue;
|
|
|
|
if (op == NULL)
|
|
LOG(llevDebug, "%s\n", scorebuf);
|
|
else
|
|
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_HISCORE, scorebuf, NULL);
|
|
|
|
}
|
|
close_and_delete(fp, comp);
|
|
}
|