1 /**
2  * Graphics - sprites management
3 
4  * Copyright (C) 1997, 1998, 1999, 2002, 2003  Seth A. Robinson
5  * Copyright (C) 2007, 2008, 2009  Sylvain Beucler
6 
7  * This file is part of GNU FreeDink
8 
9  * GNU FreeDink is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU General Public License as
11  * published by the Free Software Foundation; either version 3 of the
12  * License, or (at your option) any later version.
13 
14  * GNU FreeDink is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * General Public License for more details.
18 
19  * You should have received a copy of the GNU General Public License
20  * along with this program.  If not, see
21  * <http://www.gnu.org/licenses/>.
22  */
23 
24 #ifdef HAVE_CONFIG_H
25 #include <config.h>
26 #endif
27 
28 #include "gfx_sprites.h"
29 
30 #include <stdlib.h>
31 #include <string.h>
32 #include "SDL_image.h"
33 
34 #include "gfx.h"
35 #include "dinkvar.h"
36 
37 #include "fastfile.h"
38 #include "io_util.h"
39 #include "log.h"
40 #include "paths.h"
41 #include "dinkini.h"
42 
43 /*
44   External global variables in use:
45   seq[], GFX_k[], k[], no_running_main
46 */
47 
48 /* TODO: get rid of either k or GFX_k */
49 struct pic_info     k[MAX_SPRITES];       // Sprite data
50 struct GFX_pic_info GFX_k[MAX_SPRITES];   // Sprite data (SDL)
51 
52 struct sequence seq[MAX_SEQUENCES];
53 
54 
55 static int please_wait = 0;
56 
57 /**
58  * Free memory used by sprites. It's not much useful in itself, since
59  * it's only called when we're exiting the game, but it does avoid
60  * memory leak warnings when FreeDink is analyzed by Valgrind or other
61  * memory checkers. It also supports loading and unloading the
62  * subsystem several times.
63  */
sprites_unload(void)64 void sprites_unload(void)
65 {
66   int i = 0;
67   for (i = 0; i < MAX_SPRITES; i++)
68     {
69       if (GFX_k[i].k != NULL)
70 	SDL_FreeSurface(GFX_k[i].k);
71       GFX_k[i].k = NULL;
72     }
73   for (i = 0; i < MAX_SEQUENCES; i++)
74     {
75       if (seq[i].ini != NULL)
76 	free(seq[i].ini);
77       seq[i].ini = NULL;
78     }
79 }
80 
81 
82 /**
83  * Display a flashing "Please Wait" anim directly on the screen, just
84  * before switching to a screen that requires loading new graphics
85  * from the disk.
86  */
draw_wait()87 static void draw_wait()
88 {
89   if (seq[423].frame[8] != 0)
90     {
91 
92       if (please_wait)
93 	{
94 	  SDL_Rect dst = {232, 0, GFX_k[seq[423].frame[7]].k->w, GFX_k[seq[423].frame[7]].k->h};
95 	  SDL_BlitSurface(GFX_k[seq[423].frame[7]].k, NULL, GFX_lpDDSBack, &dst);
96 	  SDL_UpdateRects(GFX_lpDDSBack, 1, &dst);
97 	  please_wait = 0;
98 	}
99       else
100 	{
101 	  SDL_Rect dst = {232, 0, GFX_k[seq[423].frame[8]].k->w, GFX_k[seq[423].frame[8]].k->h};
102 	  SDL_BlitSurface(GFX_k[seq[423].frame[8]].k, NULL, GFX_lpDDSBack, &dst);
103 	  SDL_UpdateRects(GFX_lpDDSBack, 1, &dst);
104 	  please_wait = 1;
105 	}
106     }
107 }
108 
109 
110 /**
111  * Return the next available graphic slot
112  */
next_slot()113 static int next_slot()
114 {
115   /* Start index at 1 instead of 0. A few parts in the game rely on
116      this (e.g. if getpic(...) < 1). */
117   /* TODO: I noticed that sprite in slot 0 (by default, this would be
118      a small white square) would be displayed temporarily on the
119      screen in some situations.. */
120   int i = 1;
121   while (i < MAX_SPRITES && GFX_k[i].k != NULL)
122     i++;
123   return i;
124   /* Callee will need to check if i >= MAX_SPRITES and fail if
125      necessary */
126 }
127 
128 /**
129  * Free all graphic slots used by given sequence
130  */
free_seq(int seq_no)131 static void free_seq(int seq_no)
132 {
133   int i = 1;
134   int slot_index = -1;
135   while (i < MAX_FRAMES_PER_ABUSED_SEQUENCE+1
136 	 && (slot_index = seq[seq_no].frame[i]) != 0)
137     {
138       SDL_FreeSurface(GFX_k[slot_index].k);
139       GFX_k[slot_index].k = NULL;
140       i++;
141     }
142   /* 0 means end-of-sequence, no more frames */
143   if (i == MAX_FRAMES_PER_ABUSED_SEQUENCE+1)
144     log_error("Invalid sequence %d, just avoided a buffer overflow\n", seq_no);
145 }
146 
147 
load_sprite_pak(char seq_path_prefix[100],int seq_no,int delay,int xoffset,int yoffset,rect hardbox,int flags,int samedir)148 void load_sprite_pak(char seq_path_prefix[100], int seq_no, int delay, int xoffset, int yoffset,
149 		     rect hardbox, int flags, /*bool*/int samedir)
150 {
151   char fname[20];
152   char crap[200];
153 
154   int notanim = 0, black = 0, leftalign = 0;
155   if ((flags & DINKINI_NOTANIM) == DINKINI_NOTANIM)
156     notanim = 1;
157   if ((flags & DINKINI_BLACK) == DINKINI_BLACK)
158     black = 1;
159   if ((flags & DINKINI_LEFTALIGN) == DINKINI_LEFTALIGN)
160     leftalign = 1;
161 
162   /* If the sequence already exists, free it first */
163   free_seq(seq_no);
164 
165   if (no_running_main)
166     draw_wait();
167 
168   char *seq_dirname = pdirname(seq_path_prefix);
169   int n = strlen(seq_path_prefix) - strlen(seq_dirname)-1;
170   char *fullpath = NULL;
171   strcpy(fname, &seq_path_prefix[strlen(seq_path_prefix) - n]);
172   sprintf(crap, "%s/dir.ff", seq_dirname);
173   if (samedir)
174     fullpath = paths_dmodfile(crap);
175   else
176     fullpath = paths_fallbackfile(crap);
177 
178   if (!FastFileInit(fullpath, 5))
179     {
180       log_error("Could not load dir.ff art file %s", crap);
181       free(fullpath);
182       free(seq_dirname);
183       return;
184     }
185   free(seq_dirname);
186   /* keep 'fullpath' for error messages, free() it later */
187 
188 
189   int oo;
190   for (oo = 1; oo <= MAX_FRAMES_PER_SEQUENCE; oo++)
191     {
192       int myslot = next_slot();
193       if (myslot >= MAX_SPRITES)
194 	{
195 	  log_error("No sprite slot available! Index %d out of %d.",
196 		    myslot, MAX_SPRITES);
197 	  break;
198 	}
199 
200       char *leading_zero = NULL;
201       //load sprite
202       if (oo < 10) leading_zero = "0"; else leading_zero = "";
203       sprintf(crap, "%s%s%d.bmp", fname, leading_zero, oo);
204 
205       HFASTFILE pfile = FastFileOpen(crap);
206 
207       if (pfile == NULL)
208 	/* File not present in this fastfile - either missing file or
209 	   end of sequence */
210 	break;
211 
212       // GFX
213       SDL_RWops *rw = FastFileLock(pfile);
214       if (rw == NULL)
215 	{
216 	  /* rwops error? */
217 	  log_error("Failed to open '%s' in fastfile '%s'", crap, fullpath);
218 	}
219       else
220 	{
221 	  /* We use IMG_Load_RW instead of SDL_LoadBMP because there
222 	     is no _RW access in plain SDL. However there is no
223 	     intent to support anything else than 8bit BMPs. */
224 	  GFX_k[myslot].k = IMG_Load_RW(rw, 1); // auto free()
225 	  if (GFX_k[myslot].k == NULL)
226 	    log_error("Failed to load %s from fastfile %s: %s", crap, fullpath, SDL_GetError());
227 	}
228       if (GFX_k[myslot].k == NULL)
229 	{
230 	  log_error("Failed to load %s from fastfile %s (see error above)", crap, fullpath);
231 	  FastFileClose(pfile);
232 	  break;
233 	}
234       if (GFX_k[myslot].k->format->BitsPerPixel != 8)
235 	{
236 	  log_error("Failed to load %s from fastfile %s:"
237 		    " only 8bit paletted bitmaps are supported in dir.ff archives.",
238 		    crap, fullpath);
239 	  SDL_FreeSurface(GFX_k[myslot].k);
240 	  continue;
241 	}
242 
243       // Palettes and transparency
244 
245       /* Note: in the original engine, for efficiency, no palette
246 	 conversion was done for sprite paks - they needed to use the
247 	 Dink Palette (otherwise weird colors would appear!)) */
248       /* The engine suffered a DX limitation: palette indexes 0 and
249 	 255 fixed fixed to black and white respectively. This is also
250 	 the opposite of the Dink BMP palette indexes. This causes
251 	 troubles when skipping palette conversion (here), during
252 	 fade_down()/fade_up() (255/white pixels can't be darkened)
253 	 and in DinkC's fillcolor(index). This is why this function
254 	 replaced black with brighter black and white with darker
255 	 white. */
256       /* In FreeDink palette conversion is done in load_bmp_internal,
257 	 so we mainly care about avoiding white pixels during
258 	 fade_down(), and only because we reproduced the palette
259 	 limitation so as to support dynamic palette-changing tricks
260 	 (cf. Lyna's Story) as well as having readable white text
261 	 during fade_down(). Maintaining compatibility with
262 	 fillcolor() is also important (although forcing 0 and 255
263 	 indexes could be done in that function only). But we might
264 	 consider getting rid of it entirely. We just have to make
265 	 sure a dir.ff LEFTALIGN has no transparency, otherwise the
266 	 experience counter digits in the status bar will become
267 	 transparent. */
268       /* Not doing the conversion in truecolor mode, because at least
269 	 one D-Mod (The Blacksmith's Trail) ships with a 24bit, hence
270 	 palette-less, tiles/ts01.bmp, and this doesn't mess up
271 	 graphics in v1.08. */
272 
273       if (!truecolor)
274 	SDL_SetPalette(GFX_k[myslot].k, SDL_LOGPAL, GFX_real_pal, 0, 256);
275 
276       Uint8 *p = (Uint8 *)GFX_k[myslot].k->pixels;
277       Uint8 *last = p + GFX_k[myslot].k->h * GFX_k[myslot].k->pitch;
278 
279       if (leftalign)
280 	{
281 	  // brighten black and darken white
282 	  while (p < last)
283 	    {
284 	      if (*p == 0)        // white
285 		*p = 30;          // darker white
286 	      else if (*p == 255) // black
287 		*p = 249;         // brighter black
288 	      p++;
289 	    }
290 	}
291       else if (black)
292 	{
293 	  // darken white and set black as transparent
294 	  while (p < last)
295 	    {
296 	      if (*p == 0) // white
297 		*p = 30;   // darker white
298 	      p++;
299 	    }
300 	  SDL_SetColorKey(GFX_k[myslot].k, SDL_SRCCOLORKEY|SDL_RLEACCEL, 255);
301 	  /* Force RLE encoding now to save memory space */
302 	  SDL_BlitSurface(GFX_k[myslot].k, NULL, GFX_lpDDSTrick2, NULL);
303 	}
304       else
305 	{
306 	  // brighten black and set white as transparent
307 	  while (p < last)
308 	    {
309 	      if (*p == 255) // black in Dink palette
310 		*p = 249;    // brighter black
311 	      p++;
312 	    }
313 	  SDL_SetColorKey(GFX_k[myslot].k, SDL_SRCCOLORKEY|SDL_RLEACCEL, 0);
314 	  /* Force RLE encoding now to save memory space */
315 	  SDL_BlitSurface(GFX_k[myslot].k, NULL, GFX_lpDDSTrick2, NULL);
316 	}
317 
318       if (truecolor)
319 	{
320 	  /* We may want to convert to truecolor for possibly faster
321 	     blits. However I didn't notice a significant increase or
322 	     decrease in performances, but there's a higher memory
323 	     usage (+10MB for base Dink, more for D-Mods), so let's
324 	     not do it. */
325 	  /* 	  SDL_Surface *temp = SDL_DisplayFormat(GFX_k[myslot].k); */
326 	  /* 	  SDL_FreeSurface(GFX_k[myslot].k); */
327 	  /* 	  GFX_k[myslot].k = temp; */
328 	}
329 
330 
331       k[myslot].box.top = 0;
332       k[myslot].box.left = 0;
333       k[myslot].box.right = GFX_k[myslot].k->w;
334       k[myslot].box.bottom = GFX_k[myslot].k->h;
335 
336       /* Define the offsets / center of the image */
337       if (yoffset > 0)
338 	{
339 	  // explicitely set center
340 	  k[myslot].yoffset = yoffset;
341 	}
342       else
343 	{
344 	  if (oo > 1 && notanim)
345 	    // copy first frame info
346 	    k[myslot].yoffset = k[seq[seq_no].frame[1]].yoffset;
347 	  else
348 	    // compute default center
349 	    k[myslot].yoffset = (k[myslot].box.bottom - (k[myslot].box.bottom / 4))
350 	      - (k[myslot].box.bottom / 30);
351 	}
352 
353       if (xoffset > 0)
354 	{
355 	  // explicitely set center
356 	  k[myslot].xoffset = xoffset;
357 	}
358       else
359 	{
360 	  if (oo > 1 && notanim)
361 	    // copy first frame info
362 	    k[myslot].xoffset = k[seq[seq_no].frame[1]].xoffset;
363 	  else
364 	    // compute default center
365 	    k[myslot].xoffset = (k[myslot].box.right - (k[myslot].box.right / 2))
366 	      + (k[myslot].box.right / 6);
367 	}
368       //ok, setup main offsets, lets build the hard block
369 
370       if (hardbox.right > 0)
371 	{
372 	  //forced setting
373 	  k[myslot].hardbox.left = hardbox.left;
374 	  k[myslot].hardbox.right = hardbox.right;
375 	}
376       else
377 	{
378 	  //guess setting
379 	  int work = k[myslot].box.right / 4;
380 	  k[myslot].hardbox.left -= work;
381 	  k[myslot].hardbox.right += work;
382 	}
383 
384       if (hardbox.bottom > 0)
385 	{
386 	  k[myslot].hardbox.top = hardbox.top;
387 	  k[myslot].hardbox.bottom = hardbox.bottom;
388 	}
389       else
390 	{
391 	  int work = k[myslot].box.bottom / 10;
392 	  k[myslot].hardbox.top -= work;
393 	  k[myslot].hardbox.bottom += work;
394 	}
395       FastFileClose(pfile);
396 
397       seq[seq_no].frame[oo] = myslot;
398       seq[seq_no].delay[oo] = delay;
399     }
400   FastFileFini();
401 
402   /* Mark end-of-sequence */
403   seq[seq_no].frame[oo] = 0;
404   /* Length: inaccurate if 'set_frame_frame' is used */
405   seq[seq_no].len = oo - 1;
406 
407   if (oo == 1)
408     log_error("Sprite_load_pak error:  Couldn't load %s in %s.", crap, fullpath);
409   free(fullpath);
410 }
411 
412 
413 /* Load sprite, either from a dir.ff pack (delegated to
414    load_sprite_pak), either from a BMP file */
415 /* - seq_path_prefix: path to the file, relative to the current game (dink or dmod) */
416 /* - not_anim: reuse xoffset and yoffset from the first frame of the animation (misnomer) */
load_sprites(char seq_path_prefix[100],int seq_no,int delay,int xoffset,int yoffset,rect hardbox,int flags)417 void load_sprites(char seq_path_prefix[100], int seq_no, int delay, int xoffset, int yoffset,
418 		  rect hardbox, int flags)
419 {
420   char crap[200];
421   char *fullpath = NULL;
422   int use_fallback = 0;
423 
424   int notanim = 0, black = 0;
425   if ((flags & DINKINI_NOTANIM) == DINKINI_NOTANIM)
426     notanim = 1;
427   if ((flags & DINKINI_BLACK) == DINKINI_BLACK)
428     black = 1;
429 
430   if (no_running_main)
431     draw_wait();
432 
433 
434   /* Order: */
435   /* - dmod/.../dir.ff */
436   /* - dmod/.../...01.BMP */
437   /* - ../dink/.../dir.ff */
438   /* - ../dink/.../...01.BMP */
439   char *seq_dirname = pdirname(seq_path_prefix);
440   int exists = 0;
441 
442   if (!exists)
443     {
444       sprintf(crap, "%s/dir.ff", seq_dirname);
445       fullpath = paths_dmodfile(crap);
446       exists = exist(fullpath);
447       free(fullpath);
448       if (exists)
449 	{
450 	  free(seq_dirname);
451 	  load_sprite_pak(seq_path_prefix, seq_no, delay, xoffset, yoffset,
452 			  hardbox, flags, /*true*/1);
453 	  return;
454 	}
455     }
456 
457   if (!exists)
458     {
459       sprintf(crap, "%s01.BMP",seq_path_prefix);
460       fullpath = paths_dmodfile(crap);
461       exists = exist(fullpath);
462       free(fullpath);
463     }
464 
465   if (!exists)
466     {
467       sprintf(crap, "%s/dir.ff",  seq_dirname);
468       fullpath = paths_fallbackfile(crap);
469       exists = exist(fullpath);
470       free(fullpath);
471       if (exists)
472 	{
473 	  free(seq_dirname);
474 	  load_sprite_pak(seq_path_prefix, seq_no, delay, xoffset, yoffset,
475 			  hardbox, flags, /*false*/0);
476 	  return;
477 	}
478     }
479 
480   if (!exists)
481     {
482       /* Let's look for the BMP below */
483       use_fallback = 1;
484     }
485 
486   free(seq_dirname);
487 
488 
489   /* If the sequence already exists, free it first */
490   free_seq(seq_no);
491 
492   /* Load the whole sequence (prefix-01.bmp, prefix-02.bmp, ...) */
493   int oo;
494   for (oo = 1; oo <= MAX_FRAMES_PER_ABUSED_SEQUENCE; oo++)
495     {
496       int myslot = next_slot();
497       if (myslot >= MAX_SPRITES)
498 	{
499 	  log_error("No sprite slot available! Index %d out of %d.",
500 		    myslot, MAX_SPRITES);
501 	  break;
502 	}
503 
504       FILE *in = NULL;
505       char *leading_zero = NULL;
506       if (oo < 10) leading_zero = "0"; else leading_zero = "";
507       sprintf(crap, "%s%s%d.bmp", seq_path_prefix, leading_zero, oo);
508 
509       /* Set the pixel data */
510       if (use_fallback)
511 	in = paths_fallbackfile_fopen(crap, "rb");
512       else
513 	in = paths_dmodfile_fopen(crap, "rb");
514 
515       GFX_k[myslot].k = load_bmp_from_fp(in);
516 
517       if (GFX_k[myslot].k == NULL)
518 	{
519 	  // end of sequence
520 	  break;
521 	}
522 
523       /** Configure current frame **/
524 
525       /* Disable alpha in 32bit BMPs, like the original engine */
526       SDL_SetAlpha(GFX_k[myslot].k, 0, SDL_ALPHA_OPAQUE);
527       /* Set transparent color: either black or white */
528       if (black)
529 	SDL_SetColorKey(GFX_k[myslot].k, SDL_SRCCOLORKEY|SDL_RLEACCEL,
530 			SDL_MapRGB(GFX_k[myslot].k->format, 0, 0, 0));
531       else
532 	SDL_SetColorKey(GFX_k[myslot].k, SDL_SRCCOLORKEY|SDL_RLEACCEL,
533 			SDL_MapRGB(GFX_k[myslot].k->format, 255, 255, 255));
534 
535       /* Force RLE encoding now to save memory space */
536       SDL_BlitSurface(GFX_k[myslot].k, NULL, GFX_lpDDSTrick2, NULL);
537       /* Note: there is definitely a performance improvement when
538 	 using RLEACCEL under truecolor mode (~80%CPU -> 70%CPU) */
539 
540 
541       /* Fill in .box; this was previously done in DDSethLoad; in
542 	 the future we could get rid of the .box field and rely
543 	 directly on SDL_Surface's .w and .h fields instead: */
544       k[myslot].box.top = 0;
545       k[myslot].box.left = 0;
546       k[myslot].box.right = GFX_k[myslot].k->w;
547       k[myslot].box.bottom = GFX_k[myslot].k->h;
548 
549       /* Define the offsets / center of the image */
550       if (yoffset > 0)
551 	{
552 	  // explicitely set center
553 	  k[myslot].yoffset = yoffset;
554 	}
555       else
556 	{
557 	  if (oo > 1 && notanim)
558 	    // copy first frame info
559 	    k[myslot].yoffset = k[seq[seq_no].frame[1]].yoffset;
560 	  else
561 	    // compute default center
562 	    k[myslot].yoffset = (k[myslot].box.bottom - (k[myslot].box.bottom / 4))
563 	      - (k[myslot].box.bottom / 30);
564 	}
565 
566       if (xoffset > 0)
567 	{
568 	  // explicitely set center
569 	  k[myslot].xoffset = xoffset;
570 	}
571       else
572 	{
573 	  if (oo > 1 && notanim)
574 	    // copy first frame info
575 	    k[myslot].xoffset = k[seq[seq_no].frame[1]].xoffset;
576 	  else
577 	    // compute default center
578 	    k[myslot].xoffset = (k[myslot].box.right - (k[myslot].box.right / 2))
579 	      + (k[myslot].box.right / 6);
580 	}
581       //ok, setup main offsets, lets build the hard block
582 
583       if (hardbox.right > 0)
584 	{
585 	  //forced setting
586 	  k[myslot].hardbox.left = hardbox.left;
587 	  k[myslot].hardbox.right = hardbox.right;
588 	}
589       else
590 	{
591 	  //default setting
592 	  int work = k[myslot].box.right / 4;
593 	  k[myslot].hardbox.left -= work;
594 	  k[myslot].hardbox.right += work;
595 	}
596 
597       if (hardbox.bottom > 0)
598 	{
599 	  //forced setting
600 	  k[myslot].hardbox.top = hardbox.top;
601 	  k[myslot].hardbox.bottom = hardbox.bottom;
602 	}
603       else
604 	{
605 	  //default setting
606 	  /* eg: graphics\dink\push\ds-p2- and
607 	     graphics\effects\comets\sm-comt2\fbal2- */
608 	  int work = k[myslot].box.bottom / 10;
609 	  k[myslot].hardbox.top -= work;
610 	  k[myslot].hardbox.bottom += work;
611 	}
612 
613       seq[seq_no].frame[oo] = myslot;
614       seq[seq_no].delay[oo] = delay;
615     }
616 
617   /* Mark end-of-sequence */
618   seq[seq_no].frame[oo] = 0;
619   /* Length: inaccurate if 'set_frame_frame' is used */
620   seq[seq_no].len = oo - 1;
621 
622   /* oo == 1 => not even one sprite was loaded, error */
623   /* oo > 1 => the sequence ends */
624 
625   if (oo == 1)
626     {
627       /* First frame didn't load! */
628       log_error("load_sprites: anim '%s' not found: couldn't open '%s'", seq_path_prefix, crap);
629     }
630 }
631 
632 /**
633  * Set the dink.ini / init() line for this sequence.
634  */
seq_set_ini(int seq_no,char * line)635 void seq_set_ini(int seq_no, char *line)
636 {
637   /* Check if we are not attempting to replace a line by itself
638      (e.g. when a sequence is lazy-loaded) and free previous line. */
639   if (seq[seq_no].ini != line)
640     {
641       if (seq[seq_no].ini != NULL)
642 	free(seq[seq_no].ini);
643       seq[seq_no].ini = strdup(line);
644     }
645 }
646