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