char *rcsid_common_image_c = "$Id: image.c,v 1.18 2005/03/27 20:51:24 akirschbaum Exp $"; /* Crossfire client, a client program for the crossfire program. Copyright (C) 2001 Mark Wedel & Crossfire Development Team 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 author can be reached via e-mail to crossfire-devel@real-time.com */ /* * This file contains image related functions - this is a higher level up - * it mostly deals with the caching of the images, processing the image commands * from the server, etc. */ #include "config.h" #include #include #ifndef WIN32 #include #else #include #include #endif #include #include "client.h" #include "external.h" /* Rotate right from bsd sum. */ #define ROTATE_RIGHT(c) if ((c) & 01) (c) = ((c) >>1) + 0x80000000; else (c) >>= 1; /*#define CHECKSUM_DEBUG*/ struct FD_Cache { char name[MAX_BUF]; int fd; } fd_cache[MAX_FACE_SETS]; /* given the filename, this tries to load the data. It returns 0 success, * -1 on failure. It returns the data and len, the passed * options. csum is set to zero or unset - changes have made it such * that the caller knows whether or not the checksum matches, so there * is little point to re-do it. * data should be a buffer already allocated. */ static int load_image(char *filename, uint8 *data, int *len, int *csum) { int fd, i; char *cp; /* If the name includes an @, then that is a combined image file, * so we need to load the image a bit specially. By using these * combined image files, it reduces number of opens needed. * In fact, we keep track of which ones we have opened to improve * performance. Note that while not currently done, this combined * image scheme could be done when storing images in the players * ~/.crossfire-images directory. */ if ((cp=strchr(filename,'@'))!=NULL) { char *lp; int offset, length, last=-1; offset = atoi(cp + 1); lp = strchr(cp,':'); if (!lp) { LOG(LOG_ERROR,"common::load_image","Corrupt filename - has '@' but no ':' ?(%s)", filename); return -1; } length = atoi(lp + 1); *cp = 0; for (i=0; i= (int)(sizeof(uint32) - sizeof(char)) * 8) rot = 0; } return (hash % tablesize); } /* This function returns an index into the image_cache for * a matching entry, -1 if no match is found. */ static sint32 image_find_hash(char *str) { uint32 hash = image_hash_name(str, IMAGE_HASH), newhash; newhash = hash; do { /* No entry - return immediately */ if (image_cache[newhash].image_name == NULL) return -1; if (!strcmp(image_cache[newhash].image_name, str)) return newhash; newhash ++; if (newhash == IMAGE_HASH) newhash=0; } while (newhash != hash); /* If the hash table is full, this is bad because we won't be able to * add any new entries. */ LOG(LOG_WARNING,"common::image_find_hash","Hash table is full, increase IMAGE_CACHE size"); return -1; } static void image_remove_hash(char *imagename, Cache_Entry *ce) { int hash_entry; Cache_Entry *last; hash_entry = image_find_hash(imagename); if (hash_entry == -1) { LOG(LOG_ERROR,"common::image_remove_hash","Unable to find cache entry for %s, %s", imagename, ce->filename); return; } if (image_cache[hash_entry].cache_entry == ce) { image_cache[hash_entry].cache_entry = ce->next; free(ce->filename); free(ce); return; } last = image_cache[hash_entry].cache_entry; while (last->next && last->next != ce) last=last->next; if (!last->next) { LOG(LOG_ERROR,"common::image_rmove_hash","Unable to find cache entry for %s, %s", imagename, ce->filename); return; } last->next = ce->next; free(ce->filename); free(ce); } /* This finds and returns the Cache_Entry of the image that matches name * and checksum if has_sum is set. If has_sum is not set, we can't * do a checksum comparison. */ static Cache_Entry *image_find_cache_entry(char *imagename, uint32 checksum, int has_sum) { int hash_entry; Cache_Entry *entry; hash_entry = image_find_hash(imagename); if (hash_entry == -1) return NULL; entry = image_cache[hash_entry].cache_entry; if (has_sum) { while (entry) { if (entry->checksum == checksum) break; entry = entry->next; } } return entry; /* This could be NULL */ } /* Add a hash entry. Returns the entry we added, NULL on failure.. */ static Cache_Entry *image_add_hash(char *imagename, char *filename, uint32 checksum, uint32 public) { Cache_Entry *new_entry; uint32 hash = image_hash_name(imagename, IMAGE_HASH), newhash; newhash = hash; while (image_cache[newhash].image_name != NULL && strcmp(image_cache[newhash].image_name, imagename)) { newhash ++; if (newhash == IMAGE_HASH) newhash=0; /* If the hash table is full, can't do anything */ if (newhash == hash) { LOG(LOG_WARNING,"common::image_find_hash","Hash table is full, increase IMAGE_CACHE size"); return NULL; } } if (!image_cache[newhash].image_name) { image_cache[newhash].image_name = strdup(imagename); } /* We insert the new entry at the start of the list of the buckets * for this entry. In the case of the players entries, this probably * improves performance, presuming ones later in the file are more likely * to be used compared to those at the start of the file. */ new_entry = malloc(sizeof(struct Cache_Entry)); new_entry->filename = strdup(filename); new_entry->checksum = checksum; new_entry->public = public; new_entry->image_data = NULL; new_entry->next = image_cache[newhash].cache_entry; image_cache[newhash].cache_entry = new_entry; return new_entry; } /* Process a line from the bmaps.client file. In theory, the * format should be quite strict, as it is computer generated, * but we try to be lenient/follow some conventions. * Note that this is destructive to the data passed in line. */ static void image_process_line(char *line, uint32 public) { char imagename[MAX_BUF], filename[MAX_BUF]; uint32 checksum; if (line[0] == '#') return; /* Ignore comments */ if (sscanf(line, "%s %u %s", imagename, &checksum, filename)==3) { image_add_hash(imagename, filename, checksum, public); } else { LOG(LOG_WARNING,"common::image_process_line","Did not parse line %s properly?", line); } } void init_common_cache_data() { FILE *fp; char bmaps[MAX_BUF], inbuf[MAX_BUF]; int i; if (!want_config[CONFIG_CACHE]) return; for (i = 0; i < MAXPIXMAPNUM; i++) facetoname[i] = NULL; /* First, make sure that image_cache is nulled out */ memset(image_cache, 0, IMAGE_HASH * sizeof(struct Image_Cache)); sprintf(bmaps,"%s/bmaps.client",DATADIR); if ((fp=fopen(bmaps,"r"))!=NULL) { while (fgets(inbuf, MAX_BUF-1, fp)!=NULL) { image_process_line(inbuf, 1); } fclose(fp); } else { sprintf(inbuf,"Unable to open %s. You may wish to download and install the image file to improve performance.\n", bmaps); draw_info(inbuf, NDI_RED); } sprintf(bmaps,"%s/.crossfire/crossfire-images/bmaps.client", getenv("HOME")); if ((fp=fopen(bmaps,"r"))!=NULL) { while (fgets(inbuf, MAX_BUF-1, fp)!=NULL) { image_process_line(inbuf, 0); } fclose(fp); } /* User may not have a cache, so no error if not found */ for (i=0; iimage_data) { /* If this has image_data, then it has already been rendered */ if (!associate_cache_entry(ce, pnum)) return; } if (ce->public) sprintf(filename,"%s/%s", DATADIR, ce->filename); else sprintf(filename,"%s/.crossfire/crossfire-images/%s", getenv("HOME"), ce->filename); if (load_image(filename, data, &len, &newsum)==-1) { LOG(LOG_WARNING,"common::finish_face_cmd","file %s listed in cache file, but unable to load", filename); requestface(pnum, face); return; } } /* If we got here, we found an image and the checksum is OK. */ if (!(png_tmp = png_to_data(data, len, &nx, &ny))) { /* If the data is bad, remove it if it is in the players private cache */ LOG(LOG_WARNING,"common::finish_face_cmd","Got error on png_to_data, image=%s",face); if (ce) { if (!ce->public) unlink(filename); image_remove_hash(face,ce); } requestface(pnum, face); } /* create_and_rescale_image_from data is an external reference to a piece in * the gui section of the code. */ if (create_and_rescale_image_from_data(ce, pnum, png_tmp,nx, ny)) { LOG(LOG_WARNING,"common::finish_face_cmd","Got error on create_and_rescale_image_from_data, file=%s",filename); requestface(pnum, face); } if (png_tmp != NULL) free(png_tmp); } /* We can now connect to different servers, so we need to clear out * any old images. We try to free the data also to prevent memory * leaks. * Note that we don't touch our hashed entries - so that when we * connect to a new server, we still have all that information. */ void reset_image_cache_data() { int i; if (want_config[CONFIG_CACHE]) for (i=1; i=0 && setnum < MAX_FACE_SETS && face_info.facesets[setnum].prefix) sprintf(basename,"%s.%s", facetoname[face], face_info.facesets[setnum].prefix); else strcpy(basename, facetoname[face]); /* Decrease it by one since it will immediately get increased * in the loop below. */ setnum--; do { setnum++; sprintf(filename, "%s/.crossfire/crossfire-images/%c%c/%s.%d", getenv("HOME"), facetoname[face][0], facetoname[face][1], basename, setnum); } while (access(filename, F_OK)==-0); #ifdef WIN32 if ((tmpfile = fopen(filename,"wb"))==NULL) { #else if ((tmpfile = fopen(filename,"w"))==NULL) { #endif LOG(LOG_WARNING,"common::display_newpng","Can not open %s for writing", filename); } else { /* found a file we can write to */ fwrite(buf, buflen, 1, tmpfile); fclose(tmpfile); csum=0; for (i=0; (int)i len) return; face_info.num_images = atoi(lp); lp = cp+1; cp = strchr(lp, '\n'); if (!cp || (cp - lp) > len) return; face_info.bmaps_checksum = strtoul(lp, NULL, 10); /* need unsigned, so no atoi */ lp = cp+1; cp = strchr(lp, '\n'); while (cp && (cp - lp) <= len) { *cp++ = '\0'; /* The code below is pretty much the same as the code from the server * which loads the original faceset file. */ if (!(cps[0] = strtok(lp, ":"))) badline=1; for (i=1; i<7; i++) { if (!(cps[i] = strtok(NULL, ":"))) badline=1; } if (badline) { LOG(LOG_WARNING,"common::get_image_info","bad data, ignoring line:/%s/", lp); } else { onset = atoi(cps[0]); if (onset >=MAX_FACE_SETS) { LOG(LOG_WARNING,"common::get_image_info","setnum is too high: %d > %d", onset, MAX_FACE_SETS); } face_info.facesets[onset].prefix = strdup_local(cps[1]); face_info.facesets[onset].fullname = strdup_local(cps[2]); face_info.facesets[onset].fallback = atoi(cps[3]); face_info.facesets[onset].size = strdup_local(cps[4]); face_info.facesets[onset].extension = strdup_local(cps[5]); face_info.facesets[onset].comment = strdup_local(cps[6]); } lp = cp; cp = strchr(lp, '\n'); } face_info.have_faceset_info = 1; /* if the user has requested a specific face set and that set * is not numeric, try to find a matching set and send the * relevent setup command. */ if (face_info.want_faceset && atoi(face_info.want_faceset)==0) { for (onset=0; onset len) return; start = atoi(data); while (isspace(*cp)) cp++; lp = cp; cp = strchr(lp, ' '); if (!cp || (cp - data) > len) return; stop = atoi(lp); replyinfo_last_face = stop; /* Can't use isspace here, because it matches with tab, ascii code * 9 - this results in advancing too many spaces because * starting at image 2304, the MSB of the image number will be * 9. Using a check against space will work until we get up to * 8192 images. */ while (*cp==' ') cp++; while ((cp - data) < len) { imagenum = GetShort_String(cp); cp += 2; checksum = GetInt_String(cp); cp += 4; faceset = *cp; cp++; slen = *cp; cp++; /* Note that as is, this can break horribly if the client is missing a large number * of images - that is because it will request a whole bunch which will overflow * the servers output buffer, causing it to close the connection. * What probably should be done is for the client to just request this checksum * information in small batches so that even if the client has no local * images, requesting the entire batch won't overflow the sockets buffer - this * probably amounts to about 100 images at a time */ finish_face_cmd(imagenum, checksum, 1, cp, faceset); if (imagenum > stop) LOG(LOG_WARNING,"common::get_image_sums","Received an image beyond our range? %d > %d", imagenum, stop); cp += slen; } }