xqf/src/q3maps.c

690 lines
18 KiB
C

/* XQF - Quake server browser and launcher
* Functions for finding installed q1-q3&clones maps
* Copyright (C) 2002 Ludwig Nussel <l-n@users.sourceforge.net>
*
* 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <glib.h>
#include <unzip.h>
#include "debug.h"
#include "utils.h"
#include "q3maps.h"
struct q3mapinfo
{
char* zipfile; // may be NULL
char* levelshot;
};
static GSList* shotslist = NULL; // temporary
/** pak file code ripped from q2 */
typedef unsigned char byte;
#define IDPAKHEADER (('K'<<24)+('C'<<16)+('A'<<8)+'P')
typedef struct
{
char name[56];
int filepos, filelen;
} dpackfile_t;
typedef struct
{
int ident; // == IDPAKHEADER
int dirofs;
int dirlen;
} dpackheader_t;
static inline int LittleLong (int l) {
#if BYTE_ORDER == BIG_ENDIAN
byte b1,b2,b3,b4;
b1 = l&255;
b2 = (l>>8)&255;
b3 = (l>>16)&255;
b4 = (l>>24)&255;
return ((int)b1<<24) + ((int)b2<<16) + ((int)b3<<8) + b4;
#else
return l;
#endif
}
static void findmaps_pak(const char* packfile, GHashTable* maphash) {
dpackheader_t header;
// int numpackfiles;
int fd;
dpackfile_t info;
fd = open(packfile, O_RDONLY);
if (fd < 0) {
perror(packfile);
return;
}
if (read (fd, &header, sizeof(header))<0) {
perror(packfile);
return;
}
if (LittleLong(header.ident) != IDPAKHEADER) {
debug(0,"%s is not a packfile\n", packfile);
return;
}
header.dirofs = LittleLong (header.dirofs);
header.dirlen = LittleLong (header.dirlen);
// numpackfiles = header.dirlen / sizeof(dpackfile_t);
// printf ("packfile %s (%i files)\n", packfile, numpackfiles);
if (lseek (fd, header.dirofs, SEEK_SET) == -1) {
perror(packfile);
return;
}
while (read (fd,&info, sizeof(dpackfile_t))>0) {
if (!strncmp(info.name,"maps/",5) && !strcmp(info.name+strlen(info.name)-4,".bsp")) {
// s#maps/(.*)\.bsp#\1#
gchar* mapname = g_ascii_strdown(info.name+5, strlen(info.name)-4-5);
if (g_hash_table_lookup(maphash,mapname)) {
g_free(mapname);
}
else {
g_hash_table_insert(maphash,mapname,GUINT_TO_POINTER(-1));
}
}
}
close(fd);
return;
}
/** return pointer to last two path entries */
static inline const char* last_two_entries(const char* name) {
const char *l = name;
l = strrchr(l, '/');
if (l && l != name) {
const char *n = name;
while (n != l) {
++n;
name = n;
n = strchr(n, '/');
}
}
else
return NULL;
return name;
}
static gboolean is_q3_mapshot(const char* name) {
debug(4, "check %s", name);
if (*name == '/')
name = last_two_entries(name);
if (!name)
return FALSE;
if (g_ascii_strncasecmp(name,"levelshots/",11))
return FALSE;
if (!g_ascii_strcasecmp(name+strlen(name)-4,".jpg")
|| !g_ascii_strcasecmp(name+strlen(name)-4,".tga")) {
return TRUE;
}
return FALSE;
}
// must free
static char* is_q3_map(const char* name) {
if (!g_ascii_strncasecmp(name,"maps/",5)
&& !g_ascii_strcasecmp(name+strlen(name)-4,".bsp")) {
const char* basename = g_path_get_basename(name);
return g_strndup(basename,strlen(basename)-4);
}
return NULL;
}
// must free
static gboolean is_doom3_mapshot(const char* name) {
if (g_ascii_strncasecmp(name,"guis/assets/splash/",19))
return FALSE;
if (!g_ascii_strcasecmp(name+strlen(name)-4,".jpg")
|| !g_ascii_strcasecmp(name+strlen(name)-4,".tga")) {
return TRUE;
}
return FALSE;
}
static char* is_doom3_map(const char* name) {
if (!g_ascii_strncasecmp(name,"maps/game/",10)
&& !g_ascii_strcasecmp(name+strlen(name)-4,".map")) {
const char* basename = g_path_get_basename(name);
return g_strndup(basename,strlen(basename)-4);
}
return NULL;
}
// must free
static gboolean is_quake4_mapshot(const char* name) {
if (g_ascii_strncasecmp(name,"gfx/guis/loadscreens/",21))
return FALSE;
if (!g_ascii_strcasecmp(name+strlen(name)-4,".jpg")
|| !g_ascii_strcasecmp(name+strlen(name)-4,".tga")) {
return TRUE;
}
return FALSE;
}
static char* is_quake4_map(const char* name) {
if (!g_ascii_strncasecmp(name,"maps/",5)
&& !g_ascii_strcasecmp(name+strlen(name)-4,".map")) {
const char* basename = g_path_get_basename(name);
return g_strndup(basename,strlen(basename)-4);
}
return NULL;
}
// must free
static gboolean is_etqw_mapshot(const char* name) {
if (g_ascii_strncasecmp(name,"levelshots/thumbs/",18))
return FALSE;
if (!g_ascii_strcasecmp(name+strlen(name)-4,".jpg")
|| !g_ascii_strcasecmp(name+strlen(name)-4,".tga")) {
return TRUE;
}
return FALSE;
}
static char* is_etqw_map(const char* name) {
if (!g_ascii_strncasecmp(name,"maps/",5)
&& !g_ascii_strcasecmp(name+strlen(name)-4,".stm")) {
const char* basename = g_path_get_basename(name);
return g_strndup(basename,strlen(basename)-4);
}
return NULL;
}
static gboolean if_map_insert(const char* path, GHashTable* maphash, char* (*is_map_func)(const char* name)) {
gchar* mapname = NULL;
gchar* tmp;
mapname = is_map_func(path);
if (mapname) {
// s#maps/(.*)\.bsp#\1#
tmp = g_ascii_strdown(mapname, -1);
strcpy(mapname, tmp);
g_free(tmp);
if (g_hash_table_lookup(maphash, mapname)) {
g_free(mapname);
}
else {
g_hash_table_insert(maphash,mapname,GUINT_TO_POINTER(-1));
}
return TRUE;
}
return FALSE;
}
static gboolean if_shot_insert(const char* path,
gboolean (*is_mapshot_func)(const char* name),
const char* zipfile) {
// debug(0, "check (%d) %s", (int)(zipfile!=NULL), path);
if (is_mapshot_func(path)) {
struct q3mapinfo* mi = NULL;
size_t psize = 0, nsize;
nsize = strlen(path)+1;
if (zipfile)
psize = strlen(zipfile)+1;
mi = g_malloc0(sizeof(struct q3mapinfo) + psize + nsize);
if (!mi) return TRUE;
if (zipfile) {
mi->zipfile=(char*)mi+sizeof(struct q3mapinfo);
strcpy(mi->zipfile,zipfile);
}
mi->levelshot=(char*)mi+sizeof(struct q3mapinfo)+psize;
strcpy(mi->levelshot,path);
shotslist = g_slist_prepend(shotslist,mi);
// debug(0,"found shot %s",mi->levelshot);
return TRUE;
}
return FALSE;
}
/** open zip file and insert all contained .bsp into maphash */
static void findq3maps_zip(const char* zipfile, GHashTable* maphash,
char* (*is_map_func)(const char* name),
gboolean (*is_mapshot_func)(const char* name)) {
unzFile f;
int ret;
enum { bufsize = 256 };
char buf[bufsize] = {0};
if (!zipfile || !maphash || !is_map_func || !is_mapshot_func)
return;
f=unzOpen(zipfile);
if (!f) {
xqf_warning("could not open zip file %s",zipfile);
return;
}
for (ret = unzGoToFirstFile(f); ret == UNZ_OK; ret = unzGoToNextFile(f)) {
if (unzGetCurrentFileInfo(f,NULL,buf,bufsize,NULL,0,NULL,0) == UNZ_OK) {
if (if_map_insert(buf, maphash, is_map_func)) {
}
if (if_shot_insert(buf, is_mapshot_func, zipfile)) {
}
}
}
if (unzClose(f) != UNZ_OK) {
debug(0,"couldn't close file %s",zipfile);
}
}
gboolean quake_contains_dir(const char* name, int level, GHashTable* maphash) {
size_t len;
// printf("directory %s at level %d\n",name,level);
if (level == 0)
return TRUE;
len = strlen(name);
if (level == 1 && len > 4 && !g_ascii_strcasecmp(name+len-4,"maps"))
return TRUE;
if (level == 1 && len > 10 && !g_ascii_strcasecmp(name+len-10,"levelshots"))
return TRUE;
return FALSE;
}
void quake_contains_file(const char* name, int level, GHashTable* maphash) {
// printf("%s at level %d\n",name,level);
if (strlen(name)>4 && !g_ascii_strcasecmp(name+strlen(name)-4,".pak") && level == 1) {
findmaps_pak(name,maphash);
}
if (strlen(name)>4 && !g_ascii_strcasecmp(name+strlen(name)-4,".bsp") && level == 2) {
const gchar* basename = g_path_get_basename(name);
gchar* mapname=g_ascii_strdown(basename, strlen(basename)-4); /* g_ascii_strdown does implicit strndup */
if (g_hash_table_lookup(maphash,mapname)) {
g_free(mapname);
}
else {
g_hash_table_insert(maphash,mapname,GUINT_TO_POINTER(-1));
}
}
}
void q3_contains_file(const char* name, int level, GHashTable* maphash) {
// printf("%s at level %d\n",name,level);
if (level == 1 && !g_ascii_strcasecmp(name+strlen(name)-4,".pk3") && strlen(name) > 4) {
findq3maps_zip(name, maphash, is_q3_map, is_q3_mapshot);
}
else if (level == 2 && if_map_insert(name, maphash, is_q3_map)) {
}
else if (level == 2 && if_shot_insert(name, is_q3_mapshot, NULL)) {
}
}
static void _doom3_contains_file(const char* name, int level, GHashTable* maphash,
char* (*is_map_func)(const char* name),
gboolean (*is_mapshot_func)(const char* name)) {
// printf("%s at level %d\n",name,level);
if (level == 1 && !g_ascii_strcasecmp(name+strlen(name)-4,".pk4") && strlen(name) > 4) {
findq3maps_zip(name, maphash, is_map_func, is_mapshot_func);
}
}
void doom3_contains_file(const char* name, int level, GHashTable* maphash) {
_doom3_contains_file(name, level, maphash, is_doom3_map, is_doom3_mapshot);
}
void quake4_contains_file(const char* name, int level, GHashTable* maphash) {
_doom3_contains_file(name, level, maphash, is_quake4_map, is_quake4_mapshot);
}
void etqw_contains_file(const char* name, int level, GHashTable* maphash) {
_doom3_contains_file(name, level, maphash, is_etqw_map, is_etqw_mapshot);
}
/**
* traverse directory tree starting at startdir. Calls found_file for each file
* and found_dir for each directory (non-recoursive). Note: There is no loop
* detection so make sure found_dir returns false at some level.
**/
void traverse_dir(const char* startdir, FoundFileFunction found_file, FoundDirFunction found_dir, gpointer data) {
DIR* dir = NULL;
struct dirent* dire = NULL;
char* curdir = NULL;
GSList* dirstack = NULL;
typedef struct
{
char* name;
int level;
} DirStackEntry;
DirStackEntry* dse;
if (!startdir || !found_dir || !found_file)
return;
dse = g_new0(DirStackEntry,1);
dse->name=g_strdup(startdir);
dirstack = g_slist_prepend(dirstack,dse);
while (dirstack) {
GSList* current = dirstack;
dirstack = g_slist_remove_link(dirstack,dirstack);
dse = current->data;
curdir = dse->name;
dir = opendir(curdir);
if (!dir) {
perror(curdir);
g_free(curdir);
g_free(dse);
continue;
}
while ((dire = readdir(dir))) {
char* name = dire->d_name;
struct stat statbuf;
if (!strcmp(name,".") || !strcmp(name,".."))
continue;
name = g_strconcat(curdir,"/",name,NULL);
if (lstat(name,&statbuf)) {
perror(name);
g_free(name);
continue;
}
if (S_ISDIR(statbuf.st_mode)) {
if (found_dir(name, dse->level, data)) {
DirStackEntry* tmpdse = g_new0(DirStackEntry,1);
tmpdse->name=name;
tmpdse->level=dse->level+1;
dirstack = g_slist_prepend(dirstack,tmpdse);
}
else {
g_free(name);
}
}
else {
found_file(name, dse->level, data);
g_free(name);
}
}
closedir(dir);
g_free(curdir);
g_free(dse);
}
}
static gboolean maphashforeachremovefunc(gpointer key, gpointer value, gpointer user_data) {
g_free(key);
if (value && value!=GINT_TO_POINTER(-1))
g_free(value);
return TRUE;
}
/** free all keys and destroy maphash */
void q3_clear_maps(GHashTable* maphash) {
if (!maphash)
return;
g_hash_table_foreach_remove(maphash, maphashforeachremovefunc, NULL);
g_hash_table_destroy(maphash);
}
/** create map hash */
GHashTable* q3_init_maphash() {
return g_hash_table_new(g_str_hash,g_str_equal);
}
/** return true if mapname is contained in maphash, false otherwise */
gboolean q3_lookup_map(GHashTable* maphash, const char* mapname) {
if (g_hash_table_lookup(maphash,mapname))
return TRUE;
return FALSE;
}
/** return true if mapname is contained in maphash, false otherwise */
gboolean doom3_lookup_map(GHashTable* maphash, const char* mapname) {
if (g_hash_table_lookup(maphash,g_path_get_basename(mapname)))
return TRUE;
return FALSE;
}
/***************/
static size_t readimagefromzip(guchar** buf, const char* zipfile, const char* filename) {
unzFile zf;
int ret;
int error = 0;
size_t buflen = 0;
unz_file_info info;
g_return_val_if_fail(zipfile!=NULL,0);
g_return_val_if_fail(filename!=NULL,0);
zf = unzOpen(zipfile);
g_return_val_if_fail(zf!=NULL,0);
do
{
ret = unzLocateFile(zf,filename,2);
if (ret!=UNZ_OK) {
g_warning("unable to locate %s inside zip archive %s\n",filename,zipfile);
error = 1;
break;
}
ret = unzOpenCurrentFile(zf);
if (ret!=UNZ_OK) {
g_warning("unable to open %s inside zip archive %s\n",filename,zipfile);
error = 1;
break;
}
ret = unzGetCurrentFileInfo(zf,&info,NULL,0,NULL,0,NULL,0);
if (ret!=UNZ_OK || info.uncompressed_size <= 0) {
g_warning("unable to retrieve info on %s inside zip archive %s\n",filename,zipfile);
error = 1;
break;
}
buflen = info.uncompressed_size;
*buf = g_malloc0(buflen);
if (!*buf) {
g_error("out of memory");
error = 1;
break;
}
ret = unzReadCurrentFile(zf,*buf,buflen);
if (ret<=0) {
g_warning("unable read %s inside zip archive %s\n",filename,zipfile);
error = 1;
break;
}
ret = unzCloseCurrentFile(zf);
if (ret == UNZ_CRCERROR) {
g_warning("CRC Error: %s inside zip archive %s\n",filename,zipfile);
error = 1;
break;
}
} while(0);
unzClose(zf);
if (!error)
return buflen;
g_free(*buf);
*buf=NULL;
return 0;
}
/***************/
/** return true if mapname is contained in maphash, false otherwise */
size_t q3_lookup_mapshot(GHashTable* maphash, const char* mapname, guchar** buf) {
struct q3mapinfo* mi = g_hash_table_lookup(maphash,mapname);
if (mi && mi != GINT_TO_POINTER(-1)) {
if (!mi->zipfile) {
size_t size;
*buf = (guchar*)load_file_mem(mi->levelshot, &size);
return size;
}
else
return readimagefromzip(buf,mi->zipfile,mi->levelshot);
}
return 0;
}
/** return true if mapname is contained in maphash, false otherwise
* same as q3_lookup_mapshot except that the basename of the map is used
*/
size_t doom3_lookup_mapshot(GHashTable* maphash, const char* mapname, guchar** buf) {
struct q3mapinfo* mi = g_hash_table_lookup(maphash,g_path_get_basename(mapname));
if (mi && mi != GINT_TO_POINTER(-1) && mi->zipfile) {
return readimagefromzip(buf,mi->zipfile,mi->levelshot);
}
return 0;
}
static void process_levelshots(GHashTable* maphash) {
GSList* ptr;
for (ptr=shotslist;ptr;ptr=g_slist_next(ptr)) {
struct q3mapinfo* mi = ptr->data;
struct q3mapinfo* mih = NULL;
gchar* mapname = NULL;
const gchar* mapbase = NULL;
char* origkey = NULL;
gboolean found = FALSE;
if (!mi->levelshot || strlen(mi->levelshot) <= 4) { g_free(mi); continue; }
mapbase = g_path_get_basename(mi->levelshot);
mapname = g_ascii_strdown(mapbase, strlen(mapbase)-4); /* g_ascii_strdown does implicit strndup */
found = g_hash_table_lookup_extended(maphash,mapname,(gpointer)&origkey,(gpointer)&mih);
if (found != TRUE || mih != GINT_TO_POINTER(-1)) // not in hash or mapinfo alread defined
{
// debug(0,"drop shot %s %p",mapname,mih);
g_free(mi);
}
else {
// debug(0,"insert shot %s",mapname);
g_hash_table_insert(maphash,origkey,mi);
}
g_free(mapname);
}
g_slist_free(shotslist);
shotslist=NULL;
}
void findq3maps(GHashTable* maphash, const char* startdir) {
traverse_dir(startdir, (FoundFileFunction)q3_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
process_levelshots(maphash);
}
void findquakemaps(GHashTable* maphash, const char* startdir) {
traverse_dir(startdir, (FoundFileFunction)quake_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
}
void finddoom3maps(GHashTable* maphash, const char* startdir) {
traverse_dir(startdir, (FoundFileFunction)doom3_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
process_levelshots(maphash);
}
void findquake4maps(GHashTable* maphash, const char* startdir) {
traverse_dir(startdir, (FoundFileFunction)quake4_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
process_levelshots(maphash);
}
void findetqwmaps(GHashTable* maphash, const char* startdir) {
traverse_dir(startdir, (FoundFileFunction)etqw_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
process_levelshots(maphash);
}
#if 0
// gcc -g -Wall -O0 `glib-config --cflags` `glib-config --libs` -lz -o listq3maps q3maps.c zip/*.c debug.c
static void maphashforeachfunc(char* key, gpointer value, gpointer user_data) {
printf("%s ",key);
}
static void q3_print_maps(GHashTable* maphash) {
g_hash_table_foreach(maphash, (GHFunc) maphashforeachfunc, NULL);
printf("\n");
}
int main (void) {
GHashTable* maphash = NULL;
maphash = q3_init_maphash();
// traverse_dir("/usr/local/games/quake3", (FoundFileFunction)q3_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
// traverse_dir("/home/madmax/.q3a", (FoundFileFunction)q3_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
// traverse_dir("/usr/share/games/quakeforge", (FoundFileFunction)quake_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
// traverse_dir("/usr/share/games/quake2", (FoundFileFunction)quake_contains_file, (FoundDirFunction)quake_contains_dir, maphash);
traverse_dir("/usr/local/games/hl", (FoundFileFunction)quake_contains_file, (FoundDirFunction)quake_contains_dir, (gpointer)maphash);
q3_print_maps(maphash);
printf("%d\n",q3_lookup_map(maphash,"blub"));
printf("%d\n",q3_lookup_map(maphash,"q3dm1"));
q3_clear_maps(maphash);
return 0;
}
#endif