kobodl/score.cpp
Ville Lindholm dbc223eb84
Initial commit
Import existing source tree; original VCS history is no longer available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-28 16:35:31 +03:00

734 lines
16 KiB
C++

/*
------------------------------------------------------------
Kobo Deluxe - An enhanced SDL port of XKobo
------------------------------------------------------------
* Copyright (C) 1995, 1996 Akira Higuchi
* Copyright (C) 2002 Jeremy Sheeley
* Copyright (C) 2001-2003, 2007 David Olofson
*
* 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.
*/
#define DBG(x)
#define DBG2(x)
#define DBG3(x)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# if HAVE_SYS_NDIR_H
# include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
# include <sys/dir.h>
# endif
# if HAVE_NDIR_H
# include <ndir.h>
# endif
#endif
#if !defined(WIN32) && !defined(MACOS)
#include <pwd.h>
#endif
#include "kobolog.h"
#include "kobo.h"
#include "score.h"
score_manager_t scorefile;
/*----------------------------------------------------------
s_hiscore_t
----------------------------------------------------------*/
s_hiscore_t::s_hiscore_t()
{
profile = NULL;
name[0] = 0;
clear();
}
void s_hiscore_t::clear()
{
start_date = 0;
end_date = 0;
skill = -1;
score = 0;
start_scene = -1;
end_scene = -1;
end_lives = -1;
end_health = -1;
playtime = 0;
saves = loads = -1;
gametype = -1;
}
void s_hiscore_t::read(pfile_t &pf)
{
pf.read(start_date);
pf.read(end_date);
pf.read(skill);
pf.read(score);
pf.read(start_scene);
pf.read(end_scene);
pf.read(end_lives);
pf.read(end_health);
pf.read(playtime);
pf.read(saves);
pf.read(loads);
pf.read(gametype);
#if 0
log_printf(DLOG, "--------\n");
log_printf(DLOG, " start_date: %u\n", start_date);
log_printf(DLOG, " end_date: %u\n", end_date);
log_printf(DLOG, " skill: %d\n", skill);
log_printf(DLOG, " gametype: %d\n", gametype);
log_printf(DLOG, " score: %u\n", score);
log_printf(DLOG, " start_scene: %d\n", start_scene);
log_printf(DLOG, " end_scene: %d\n", end_scene);
log_printf(DLOG, " end_lives: %d\n", end_lives);
log_printf(DLOG, " end_health: %d\n", end_health);
log_printf(DLOG, " playtime: %u\n", playtime);
log_printf(DLOG, " saves: %d\n", saves);
log_printf(DLOG, " loads: %d\n", loads);
log_printf(DLOG, "--------\n");
#endif
}
void s_hiscore_t::write(pfile_t &pf)
{
pf.write(start_date);
pf.write(end_date);
pf.write(skill);
pf.write(score);
pf.write(start_scene);
pf.write(end_scene);
pf.write(end_lives);
pf.write(end_health);
pf.write(playtime);
pf.write(saves);
pf.write(loads);
pf.write(gametype);
}
/*----------------------------------------------------------
s_profile_t
----------------------------------------------------------*/
s_profile_t::s_profile_t()
{
for(int i = 0; i < HISCORE_SAVE_MAX; ++i)
hiscoretab[i].profile = this;
filename = NULL;
}
s_profile_t::~s_profile_t()
{
free(filename);
}
void s_profile_t::clear()
{
best_score = 0;
last_scene = 0;
name[0] = 0;
version = 0;
skill = SKILL_CLASSIC;
handicap = 0;
color1 = -1;
color2 = -1;
hiscores = 0;
for(int i = 0; i < HISCORE_SAVE_MAX; ++i)
hiscoretab[i].clear();
}
int s_profile_t::load(const char *fn)
{
log_printf(D3LOG, "s_profile_t::load('%s')\n", fn);
free(filename);
filename = strdup(fn);
FILE *f = fopen(filename, "rb");
if(!f)
{
log_printf(ELOG, "Failed to open player profile '%s'!\n", fn);
return -1;
}
pfile_t pf(f);
clear();
// Check if this is an old broken 73 byte file written by a
// previous version for Win32. If it is, skip the first byte.
fseek(f, 0, SEEK_END);
if(ftell(f) == 73)
{
log_printf(ELOG, "Broken score file '%s' detected and fixed.\n", fn);
fseek(f, 1, SEEK_SET);
}
else
fseek(f, 0, SEEK_SET);
pf.read(best_score);
pf.read(last_scene);
pf.read(name, sizeof(name));
log_printf(DLOG, "name: %s\n", name);
log_printf(DLOG, " best_score: %u\n", best_score);
log_printf(DLOG, " last_scene: %u\n", last_scene);
pf.buffer_close();
if(pf.status() < 0)
{
log_printf(ELOG, "Error reading player profile '%s'!\n", fn);
fclose(f);
return -1;
}
//Kobo Deluxe profile file format 1+
int chunks = 0;
while(!feof(f))
{
if(pf.chunk_read() < 0)
{
pf.status(); // Just EOF - no big deal...
break;
}
switch(pf.chunk_type())
{
case MAKE_4CC('P', 'R', 'O', 'F'):
log_printf(D2LOG, "Chunk 'PROF'; %d bytes.\n",
pf.chunk_size());
if(pf.chunk_size() >= 20)
{
pf.read(version);
pf.read(skill);
pf.read(handicap);
pf.read(color1);
pf.read(color2);
}
else
{
log_printf(WLOG, "WARNING: Truncated 'PROF' chunk"
" in player profile '%s'!\n", fn);
}
break;
case MAKE_4CC('H', 'I', 'S', 'C'):
log_printf(D2LOG, "Chunk 'HISC'; %d bytes.\n",
pf.chunk_size());
if(hiscores < HISCORE_SAVE_MAX)
hiscoretab[hiscores++].read(pf);
break;
default:
{
char tp[5];
int t = pf.chunk_type();
tp[0] = (t >> 24) & 0xff;
tp[1] = (t >> 16) & 0xff;
tp[2] = (t >> 8) & 0xff;
tp[3] = t & 0xff;
tp[4] = 0;
log_printf(D2LOG, "Unknown chunk '%s'; %d bytes.\n",
tp, pf.chunk_size());
}
break;
}
++chunks;
pf.chunk_end();
}
if(!chunks)
{
log_printf(D2LOG, "Old scorefile.\n");
//Construct a "fake" highscore entry from the old data.
hiscores = 1;
hiscoretab[0].skill = SKILL_CLASSIC; //Can't tell speed.
hiscoretab[0].gametype = GAME_SINGLE; //No others available.
hiscoretab[0].score = best_score;
hiscoretab[0].end_scene = last_scene; //Likely, but...
hiscoretab[0].saves = 0; //Not implemented.
hiscoretab[0].loads = 0; //Not implemented.
}
fclose(f);
return pf.status();
}
int s_profile_t::save()
{
log_printf(D3LOG, "s_profile_t::save('%s')\n", filename);
#ifndef WIN32
umask(022);
#endif
if(!filename)
{
log_printf(ELOG, "Failed to save player profile - no file name!\n");
return -1;
}
//The "safe list"; platforms that do not have symlinks:
#if !defined(WIN32)
#ifdef HAVE_STAT
// We will not write score files via symlinks!
struct stat statbuf;
if(lstat(filename, &statbuf) < 0)
{
log_printf(ELOG, "Cannot stat player profile '%s'! "
"Could be a symlink - will NOT write.\n",
filename);
return -1;
}
if( (statbuf.st_mode & S_IFLNK) == S_IFLNK )
{
log_printf(ELOG, "Player profile '%s' is a symlink! "
"I will NOT write through symlinks.\n",
filename);
return -1;
}
#else
#warning ================= SECURITY HAZARD =================
#warning If this platform has symlinks or similar, please
#warning add an appropriate test. If not, add your platform
#warning to the "safe list". Either way, if you read this,
#warning post a bug report to the Kobo Deluxe maintainer.
#warning ================= SECURITY HAZARD =================
#error (Remove this line to compile anyway.)
#endif /* HAVE_STAT */
#endif /* "Safe list" */
FILE *f = fopen(filename, "wb");
if(!f)
{
log_printf(ELOG, "Failed to create player profile '%s'!\n",
filename);
return -1;
}
fseek(f, 0, SEEK_SET);
pfile_t pf(f);
// Write old XKobo stuff
pf.write(best_score);
pf.write(last_scene);
pf.write(name, sizeof(name));
pf.buffer_write();
// Write profile header
if(pf.chunk_write(MAKE_4CC('P', 'R', 'O', 'F')) < 0)
{
fclose(f);
return pf.status();
}
pf.write((unsigned int)PROFILE_VERSION);
pf.write(skill);
pf.write(handicap);
pf.write(color1);
pf.write(color2);
pf.chunk_end();
// Write highscores
for(unsigned int i = 0; i < hiscores; ++i)
{
if(pf.chunk_write(MAKE_4CC('H', 'I', 'S', 'C')) < 0)
{
fclose(f);
return pf.status();
}
hiscoretab[i].write(pf);
pf.chunk_end();
}
fclose(f);
return pf.status();
}
s_hiscore_t *s_profile_t::best_hiscore()
{
if(!hiscores)
return NULL;
s_hiscore_t *hs = hiscoretab;
for(unsigned int i = 1; i < hiscores; ++i)
if(hiscoretab[i].score > hs->score)
hs = hiscoretab + i;
return hs;
}
/*----------------------------------------------------------
score_manager_t
----------------------------------------------------------*/
score_manager_t::score_manager_t()
{
}
score_manager_t::~score_manager_t()
{
}
void score_manager_t::init()
{
const char *p;
// The logic to this is:
// 1. Check to see if they have an old score file.
// If they do, rename it to follow the new naming format
// which is X.UID. On Win32 (which doesn't have a UID), the
// UID is set to 42.
// 2. Load all of the X.UID scores into our array.
// 3. Determine what the last used file was.
// Change as of 0.4-pre7: "score files" are upgraded
// to "player profile files", which contain IFF style
// chunks beyond the 74 XKobo compatible bytes.
#ifdef WIN32
/*
FIXME: This should be properly implemented on Windoze - if we
FIXME: decide that it's actually much point. (Users, security etc
FIXME: are rather foreign matters to the average winuser...)
*/
int nUID = 42;
#else
uid_t nUID = getuid();
#endif
// Now load all of the files. Start with trying to get the old
// file name first. That file's information will become games[0],
// and then the old file name will be deleted.
int numberSaveGamesFound = 0;
for(int i = 0; i < MAX_PROFILES; i++)
profiles[i].clear();
char buf[1024];
snprintf(buf, sizeof(buf), "SCORES>>%u", nUID);
p = fmap->get(buf);
if(p)
{
if(profiles[numberSaveGamesFound].load(p) < 0)
log_printf(WLOG, "WARNING: Can't read score file!\n");
else
{
numberSaveGamesFound = 1;
log_printf(ULOG, "Old score file \"%s\" found"
" and imported.\n", p);
}
}
// Search through all registered score directories
// for all of the files that end in .UID
struct stat st_buffer;
fmap->get_all("SCORES>>", FM_DIR);
char UIDbuf[12];
snprintf(UIDbuf, sizeof(UIDbuf), ".%u", nUID);
while(1)
{
const char *path = fmap->get_next();
if(!path)
break;
// if the last characters in the file name is the string
// UIDbuf...
char *tmpptr = strstr((char *)path, UIDbuf);
if(tmpptr != NULL && (*(tmpptr + strlen(UIDbuf)) == '\0'))
{
if(stat(path, &st_buffer))
continue;
if(!S_ISREG(st_buffer.st_mode))
continue;
if(profiles[numberSaveGamesFound].load(path) < 0)
continue;
if((++numberSaveGamesFound) >= MAX_PROFILES)
break;
}
}
numProfiles = numberSaveGamesFound;
// If there are no save files, introbase in states.cpp will take
// them to the enter_name screen.
gather_high_scores();
print_high_scores();
}
int score_manager_t::addPlayer(const char *name)
{
log_printf(D3LOG, "creating profile %d\n", numProfiles);
if(numProfiles == MAX_PROFILES)
return -1;
int ret = 0;
#ifdef WIN32
int userid = 42;
#else
uid_t userid = getuid();
#endif
profiles[numProfiles].clear();
strncpy(profiles[numProfiles].name, name, SCORE_NAME_LEN);
// TODO: Somewhere around here would be a nice place to check if the
// TODO: scoredir exists, and if not, try to create one.
// This means that we always create new profiles in the first
// scoredir registered. (Loaded score files are written back
// where they were read from when changed.)
char buf[1024];
snprintf(buf, sizeof(buf), "SCORES>>%s.%u", name, userid);
const char *fn = fmap->get(buf, FM_FILE_CREATE);
free(profiles[numProfiles].filename);
if(fn)
profiles[numProfiles].filename = strdup(fn);
else
{
profiles[numProfiles].filename = NULL;
log_printf(ELOG, "ERROR: Couldn't resolve player profile"
" path '%s'!\n", buf);
ret = -2;
}
log_printf(ULOG, "Created new player profile '%s' for player '%s'\n",
fn ? fn : "<NO FILE!>", profiles[numProfiles].name);
currentProfile = numProfiles;
// Try to create the high score file.
if(profiles[numProfiles].save() < 0)
ret = -3;
numProfiles++;
return ret;
}
void score_manager_t::select_profile(int prof)
{
log_printf(D3LOG, "score_manager_t::select_profile(%d)\n", prof);
currentProfile = prof;
}
// Rules:
// * Highest score gets highest priority.
// * If the maximum number of entries is
// exceeded, the lowest score entry is
// overwritten.
// * best_score and last_scene of the
// profile are bumped if exceeded.
void score_manager_t::record(s_hiscore_t *entry, int force)
{
log_printf(D3LOG, "score_manager_t::record\n");
s_profile_t *p = profiles + currentProfile;
int write;
if(force)
write = 1;
else
write = 0;
if(entry->score > p->best_score)
{
p->best_score = entry->score;
write = 1;
}
if(entry->end_scene > p->last_scene)
{
p->last_scene = entry->end_scene;
write = 1;
}
int si = -1;
if(p->hiscores < HISCORE_SAVE_MAX)
{
// Grab next free slot
si = p->hiscores++;
}
else
{
// Find worst score
//FIXME: Pick old or new when scores are equal?
unsigned int worst_score = entry->score;
for(int i = 0; i < HISCORE_SAVE_MAX; ++i)
if(p->hiscoretab[i].score <= worst_score)
{
si = i;
worst_score = p->hiscoretab[i].score;
}
}
if(si >= 0)
{
// Copy and "fix" entry...
log_printf(DLOG, "Saving in slot %d.\n", si);
p->hiscoretab[si] = *entry;
p->hiscoretab[si].profile = p;
memcpy(p->hiscoretab[si].name, p->name, SCORE_NAME_LEN);
write = 1;
}
else
log_printf(DLOG, "Not saved.\n");
if(write)
{
log_printf(DLOG, "Writing file %s for player %s...\n",
p->filename, p->name);
p->save();
gather_high_scores();
print_high_scores();
log_printf(DLOG, " Done!\n");
}
}
int s_table_cmp(const void *_a, const void *_b)
{
s_hiscore_t *a = (s_hiscore_t *)_a;
s_hiscore_t *b = (s_hiscore_t *)_b;
#if 0
if(a->gametype < b->gametype)
return 1;
else if(a->gametype > b->gametype)
return -1;
else
#endif
if(a->score < b->score)
return 1;
else if(a->score > b->score)
return -1;
return 0;
}
void score_manager_t::gather_high_scores(int placeholders)
{
const int sc[] = {
10000, 9000, 8000, 7000, 6000,
5000, 4000, 3000, 2000, 1000,
0
};
const int st[] = {
10, 9, 8, 7, 6,
5, 4, 3, 2, 1
};
const char *nm[] = {
"DAVID OLOFSON",
"AKIRA HIGUCHI",
"MASANAO IZUMO",
"MAX HORN",
"SAMUEL HART",
"ANDREAS",
"TRICK",
"RIKI",
"GEORGIE",
"JEREMY"
};
for(int i = 0; i < MAX_HIGHSCORES; ++i)
high_tbl[i].clear();
s_profile_t p;
highs = 0;
fmap->get_all("SCORES>>", FM_DIR);
while(highs < MAX_HIGHSCORES)
{
const char *path = fmap->get_next();
if(!path)
break;
if(p.load(path) < 0)
continue;
s_hiscore_t *hs = p.best_hiscore();
if(!hs)
continue;
high_tbl[highs] = *hs;
memcpy(high_tbl[highs].name, p.name, SCORE_NAME_LEN);
high_tbl[highs].profile = NULL;
++highs;
}
if(placeholders)
{
//Throw some nice looking names in, if there's room. ;-)
int i = 0;
while(highs < MAX_HIGHSCORES)
{
if(!sc[i])
break;
memcpy(high_tbl[highs].name, nm[i], SCORE_NAME_LEN);
high_tbl[highs].skill = SKILL_CLASSIC;
high_tbl[highs].gametype = GAME_SINGLE;
high_tbl[highs].score = sc[i];
high_tbl[highs].start_scene = 0;
high_tbl[highs].end_scene = st[i];
++i;
++highs;
}
}
qsort(high_tbl, highs, sizeof(high_tbl[0]), s_table_cmp);
}
int score_manager_t::highscore(int prof)
{
if(-1 == prof)
return profiles[currentProfile].last_scene;
else if(-2 == prof)
return high_tbl[0].score;
else
return profiles[prof].best_score;
}
void score_manager_t::print_high_scores()
{
if(!highs)
{
log_printf(WLOG, "No hiscore entries found!\n");
return;
}
log_printf(ULOG, "Name Score Start End\n");
for(unsigned int j = 0; j < highs; j++)
{
log_printf(ULOG, "%-20s %9u %5d %3d\n", high_tbl[j].name,
high_tbl[j].score,
high_tbl[j].start_scene,
high_tbl[j].end_scene);
}
}