1 #include "anim/packunpack.h"
2 #include "globalincs/globals.h"
3 #include "graphics/2d.h"
4 #include "graphics/generic.h"
5 #define BMPMAN_INTERNAL
6 #include "bmpman/bm_internal.h"
7 #ifdef _WIN32
8 #include <windows.h>	// for MAX_PATH
9 #else
10 #define MAX_PATH	255
11 #endif
12 //#define TIMER
13 #ifdef TIMER
14 #include "io/timer.h"
15 #endif
16 
17 //we check background type to avoid messed up colours for ANI
18 #define ANI_BPP_CHECK		(ga->ani.bg_type == BM_TYPE_PCX) ? 16 : 32
19 
20 // These two functions find if a bitmap or animation exists by filename, no extension needed.
generic_bitmap_exists(const char * filename)21 bool generic_bitmap_exists(const char *filename)
22 {
23 	return cf_exists_full_ext(filename, CF_TYPE_ANY, BM_NUM_TYPES, bm_ext_list) != 0;
24 }
25 
generic_anim_exists(const char * filename)26 bool generic_anim_exists(const char *filename)
27 {
28 	return cf_exists_full_ext(filename, CF_TYPE_ANY, BM_ANI_NUM_TYPES, bm_ani_ext_list) != 0;
29 }
30 
31 // Goober5000
generic_anim_init_and_stream(generic_anim * ga,const char * anim_filename,BM_TYPE bg_type,bool attempt_hi_res)32 int generic_anim_init_and_stream(generic_anim *ga, const char *anim_filename, BM_TYPE bg_type, bool attempt_hi_res)
33 {
34 	int stream_result = -1;
35 	char filename[NAME_LENGTH];
36 	char *p;
37 
38 	Assert(ga != NULL);
39 	Assert(anim_filename != NULL);
40 
41 	// hi-res support
42 	if (attempt_hi_res && (gr_screen.res == GR_1024)) {
43 		// attempt to load a hi-res animation
44 		memset(filename, 0, NAME_LENGTH);
45 		strcpy_s(filename, "2_");
46 		strncat(filename, anim_filename, NAME_LENGTH - 3);
47 
48 		// remove extension
49 		p = strchr(filename, '.');
50 		if(p) {
51 			*p = '\0';
52 		}
53 
54 		// attempt to stream the hi-res ani
55 		generic_anim_init(ga, filename);
56 		ga->ani.bg_type = bg_type;
57 		stream_result = generic_anim_stream(ga);
58 	}
59 
60 	// we failed to stream hi-res, or we aren't running in hi-res, so try low-res
61 	if (stream_result < 0) {
62 		strcpy_s(filename, anim_filename);
63 
64 		// remove extension
65 		p = strchr(filename, '.');
66 		if(p) {
67 			*p = '\0';
68 		}
69 
70 		// attempt to stream the low-res ani
71 		generic_anim_init(ga, filename);
72 		ga->ani.bg_type = bg_type;
73 		stream_result = generic_anim_stream(ga);
74 	}
75 
76 	return stream_result;
77 }
78 
79 // Goober5000
generic_anim_init(generic_anim * ga)80 void generic_anim_init(generic_anim *ga)
81 {
82 	generic_anim_init(ga, NULL);
83 }
84 
85 // Goober5000
generic_anim_init(generic_anim * ga,const char * filename)86 void generic_anim_init(generic_anim *ga, const char *filename)
87 {
88 	if (filename != NULL)
89 		strcpy_s(ga->filename, filename);
90 	else
91 		memset(ga->filename, 0, MAX_FILENAME_LEN);
92 	ga->first_frame = -1;
93 	ga->num_frames = 0;
94 	ga->keyframe = 0;
95 	ga->keyoffset = 0;
96 	ga->current_frame = 0;
97 	ga->previous_frame = -1;
98 	ga->direction = GENERIC_ANIM_DIRECTION_FORWARDS;
99 	ga->done_playing = 0;
100 	ga->total_time = 0.0f;
101 	ga->anim_time = 0.0f;
102 
103 	//we only care about the stuff below if we're streaming
104 	ga->ani.animation = NULL;
105 	ga->ani.instance = NULL;
106 	ga->ani.bg_type = BM_TYPE_NONE;
107 	ga->type = BM_TYPE_NONE;
108 	ga->streaming = 0;
109 	ga->buffer = NULL;
110 	ga->height = 0;
111 	ga->width = 0;
112 	ga->bitmap_id = -1;
113 	ga->use_hud_color = false;
114 }
115 
116 // CommanderDJ - same as generic_anim_init, just with an SCP_string
generic_anim_init(generic_anim * ga,const SCP_string & filename)117 void generic_anim_init(generic_anim *ga, const SCP_string& filename)
118 {
119 	generic_anim_init(ga);
120 	filename.copy(ga->filename, MAX_FILENAME_LEN - 1);
121 }
122 
123 // Goober5000
generic_bitmap_init(generic_bitmap * gb,const char * filename)124 void generic_bitmap_init(generic_bitmap *gb, const char *filename)
125 {
126 	if (filename == NULL) {
127 		gb->filename[0] = '\0';
128 	} else {
129 		strncpy(gb->filename, filename, MAX_FILENAME_LEN - 1);
130 	}
131 
132 	gb->bitmap_id = -1;
133 }
134 
135 // Goober5000
136 // load a generic_anim
137 // return 0 is successful, otherwise return -1
generic_anim_load(generic_anim * ga)138 int generic_anim_load(generic_anim *ga)
139 {
140 	int fps;
141 
142 	if ( !VALID_FNAME(ga->filename) )
143 		return -1;
144 
145 	ga->first_frame = bm_load_animation(ga->filename, &ga->num_frames, &fps, &ga->keyframe, &ga->total_time);
146 	//mprintf(("generic_anim_load: %s - keyframe = %d\n", ga->filename, ga->keyframe));
147 
148 	if (ga->first_frame < 0)
149 		return -1;
150 
151 	ga->done_playing = 0;
152 	ga->anim_time = 0.0f;
153 
154 	return 0;
155 }
156 
generic_anim_stream(generic_anim * ga,const bool cache)157 int generic_anim_stream(generic_anim *ga, const bool cache)
158 {
159 	CFILE *img_cfp = NULL;
160 	int anim_fps = 0;
161 	int bpp;
162 
163 	ga->type = BM_TYPE_NONE;
164 
165 	auto res = cf_find_file_location_ext(ga->filename, BM_ANI_NUM_TYPES, bm_ani_ext_list, CF_TYPE_ANY, false);
166 
167 	// could not be found, or is invalid for some reason
168 	if ( !res.found )
169 		return -1;
170 
171 	//make sure we can open it
172 	img_cfp = cfopen_special(res, "rb", CF_TYPE_ANY);
173 
174 	if (img_cfp == NULL) {
175 		return -1;
176 	}
177 
178 	strcat_s(ga->filename, bm_ani_ext_list[res.extension_index]);
179 	ga->type = bm_ani_type_list[res.extension_index];
180 	//seek to the end
181 	cfseek(img_cfp, 0, CF_SEEK_END);
182 
183 	cfclose(img_cfp);
184 
185 	if(ga->type == BM_TYPE_ANI) {
186 		bpp = ANI_BPP_CHECK;
187 		if(ga->use_hud_color)
188 			bpp = 8;
189 		if (ga->ani.animation == nullptr) {
190 			ga->ani.animation = anim_load(ga->filename, CF_TYPE_ANY, 0);
191 		}
192 		if (ga->ani.instance == nullptr) {
193 			ga->ani.instance = init_anim_instance(ga->ani.animation, bpp);
194 		}
195 
196 	#ifndef NDEBUG
197 		// for debug of ANI sizes
198 		strcpy_s(ga->ani.animation->name, ga->filename);
199 	#endif
200 
201 		ga->num_frames = ga->ani.animation->total_frames;
202 		anim_fps = ga->ani.animation->fps;
203 		ga->height = ga->ani.animation->height;
204 		ga->width = ga->ani.animation->width;
205 		ga->buffer = ga->ani.instance->frame;
206 		ga->bitmap_id = bm_create(bpp, ga->width, ga->height, ga->buffer, (bpp==8)?BMP_AABITMAP:0);
207 		ga->ani.instance->last_bitmap = -1;
208 
209 		ga->ani.instance->file_offset = ga->ani.animation->file_offset;
210 		ga->ani.instance->data = ga->ani.animation->data;
211 
212 		ga->previous_frame = -1;
213 	}
214 	else if (ga->type == BM_TYPE_PNG) {
215 		if (ga->png.anim == nullptr) {
216 			try {
217 				ga->png.anim = new apng::apng_ani(ga->filename, cache);
218 			}
219 			catch (const apng::ApngException& e) {
220 				mprintf(("Failed to load apng: %s\n", e.what() ));
221 				delete ga->png.anim;
222 				ga->png.anim = nullptr;
223 				ga->type = BM_TYPE_NONE;
224 				return -1;
225 			}
226 			nprintf(("apng", "apng read OK (%ix%i@%i) duration (%f)\n", ga->png.anim->w, ga->png.anim->h,
227 					ga->png.anim->bpp, ga->png.anim->anim_time));
228 		}
229 		ga->png.anim->goto_start();
230 		ga->current_frame = 0;
231 		ga->png.previous_frame_time = 0.0f;
232 		ga->num_frames = ga->png.anim->nframes;
233 		ga->height = ga->png.anim->h;
234 		ga->width = ga->png.anim->w;
235 		ga->previous_frame = -1;
236 		ga->buffer = ga->png.anim->frame.data.data();
237 		ga->bitmap_id = bm_create(ga->png.anim->bpp, ga->width, ga->height, ga->buffer, 0);
238 	}
239 	else {
240 		bpp = 32;
241 		if(ga->use_hud_color)
242 			bpp = 8;
243 		bm_load_and_parse_eff(ga->filename, CF_TYPE_ANY, &ga->num_frames, &anim_fps, &ga->keyframe, 0);
244 		char *p = strrchr( ga->filename, '.' );
245 		if ( p )
246 			*p = 0;
247 		char frame_name[MAX_FILENAME_LEN];
248 		if (snprintf(frame_name, MAX_FILENAME_LEN, "%s_0000", ga->filename) >= MAX_FILENAME_LEN) {
249 			// Make sure the string is null terminated
250 			frame_name[MAX_FILENAME_LEN - 1] = '\0';
251 		}
252 		ga->bitmap_id = bm_load(frame_name);
253 		if(ga->bitmap_id < 0) {
254 			mprintf(("Cannot find first frame for eff streaming. eff Filename: %s\n", ga->filename));
255 			return -1;
256 		}
257 		if (snprintf(frame_name, MAX_FILENAME_LEN, "%s_0001", ga->filename) >= MAX_FILENAME_LEN) {
258 			// Make sure the string is null terminated
259 			frame_name[MAX_FILENAME_LEN - 1] = '\0';
260 		}
261 		ga->eff.next_frame = bm_load(frame_name);
262 		bm_get_info(ga->bitmap_id, &ga->width, &ga->height);
263 		ga->previous_frame = 0;
264 	}
265 
266 	// keyframe info
267 	if (ga->type == BM_TYPE_ANI) {
268 		//we only care if there are 2 keyframes - first frame, other frame to jump to for ship/weapons
269 		//mainhall door anis hav every frame as keyframe, so we don't care
270 		//other anis only have the first frame
271 		if(ga->ani.animation->num_keys == 2) {
272 			int key1 = ga->ani.animation->keys[0].frame_num;
273 			int key2 = ga->ani.animation->keys[1].frame_num;
274 
275 			if (key1 < 0 || key1 >= ga->num_frames) key1 = -1;
276 			if (key2 < 0 || key2 >= ga->num_frames) key2 = -1;
277 
278 			// some retail anis have their keyframes reversed
279 			// and some have their keyframes out of bounds
280 			if (key1 >= 0 && key1 >= key2) {
281 				ga->keyframe = ga->ani.animation->keys[0].frame_num;
282 				ga->keyoffset = ga->ani.animation->keys[0].offset;
283 			}
284 			else if (key2 >= 0 && key2 >= key1) {
285 				ga->keyframe = ga->ani.animation->keys[1].frame_num;
286 				ga->keyoffset = ga->ani.animation->keys[1].offset;
287 			}
288 		}
289 	}
290 
291 	ga->streaming = 1;
292 
293 	if (ga->type == BM_TYPE_PNG) {
294 		ga->total_time = ga->png.anim->anim_time;
295 	}
296 	else {
297 		if (anim_fps == 0) {
298 			Error(LOCATION, "animation (%s) has invalid fps of zero, fix this!", ga->filename);
299 		}
300 		ga->total_time = ga->num_frames / (float) anim_fps;
301 	}
302 	ga->done_playing = 0;
303 	ga->anim_time = 0.0f;
304 
305 	return 0;
306 }
307 
generic_bitmap_load(generic_bitmap * gb)308 int generic_bitmap_load(generic_bitmap *gb)
309 {
310 	if ( !VALID_FNAME(gb->filename) )
311 		return -1;
312 
313 	gb->bitmap_id = bm_load(gb->filename);
314 
315 	if (gb->bitmap_id < 0)
316 		return -1;
317 
318 	return 0;
319 }
320 
generic_anim_unload(generic_anim * ga)321 void generic_anim_unload(generic_anim *ga)
322 {
323 	if(ga->num_frames > 0) {
324 		if(ga->streaming) {
325 			if(ga->type == BM_TYPE_ANI) {
326 				free_anim_instance(ga->ani.instance);
327 				anim_free(ga->ani.animation);
328 			}
329 			if(ga->type == BM_TYPE_EFF) {
330 				if(ga->eff.next_frame >= 0)
331 					bm_release(ga->eff.next_frame);
332 				if(ga->bitmap_id >= 0)
333 					bm_release(ga->bitmap_id);
334 			}
335 			if(ga->type == BM_TYPE_PNG) {
336 				if(ga->bitmap_id >= 0)
337 					bm_release(ga->bitmap_id);
338 				if (ga->png.anim != nullptr) {
339 					delete ga->png.anim;
340 					ga->png.anim = nullptr;
341 				}
342 			}
343 		}
344 		else {
345 			//trying to release the first frame will release ALL frames
346 			bm_release(ga->first_frame);
347 		}
348 		if(ga->buffer) {
349 			bm_release(ga->bitmap_id);
350 		}
351 	}
352 	generic_anim_init(ga, NULL);
353 }
354 
355 //for timer debug, #define TIMER
generic_render_eff_stream(generic_anim * ga)356 void generic_render_eff_stream(generic_anim *ga)
357 {
358 	if(ga->current_frame == ga->previous_frame)
359 		return;
360 	ubyte bpp = 32;
361 	if(ga->use_hud_color)
362 		bpp = 8;
363 	#ifdef TIMER
364 		int start_time = timer_get_fixed_seconds();
365 	#endif
366 
367 	#ifdef TIMER
368 		mprintf(("=========================\n"));
369 		mprintf(("frame: %d\n", ga->current_frame));
370 	#endif
371 		char frame_name[MAX_FILENAME_LEN];
372 		if (snprintf(frame_name, MAX_FILENAME_LEN, "%s_%.4d", ga->filename, ga->current_frame) >= MAX_FILENAME_LEN) {
373 			// Make sure the string is null terminated
374 			frame_name[MAX_FILENAME_LEN - 1] = '\0';
375 		}
376 		if(bm_reload(ga->eff.next_frame, frame_name) == ga->eff.next_frame)
377 		{
378 			bitmap* next_frame_bmp = bm_lock(ga->eff.next_frame, bpp, (bpp==8)?BMP_AABITMAP:BMP_TEX_NONCOMP, true);
379 			if(next_frame_bmp->data)
380 				gr_update_texture(ga->bitmap_id, bpp, (ubyte*)next_frame_bmp->data, ga->width, ga->height);
381 			bm_unlock(ga->eff.next_frame);
382 			bm_unload(ga->eff.next_frame, 0, true);
383 			if (ga->current_frame == ga->num_frames-1)
384 			{
385 				if (snprintf(frame_name, MAX_FILENAME_LEN, "%s_0001", ga->filename) >= MAX_FILENAME_LEN) {
386 					// Make sure the string is null terminated
387 					frame_name[MAX_FILENAME_LEN - 1] = '\0';
388 				}
389 				bm_reload(ga->eff.next_frame, frame_name);
390 			}
391 		}
392 	#ifdef TIMER
393 		mprintf(("end: %d\n", timer_get_fixed_seconds() - start_time));
394 		mprintf(("=========================\n"));
395 	#endif
396 }
397 
generic_render_ani_stream(generic_anim * ga)398 void generic_render_ani_stream(generic_anim *ga)
399 {
400 	int i;
401 	int bpp = ANI_BPP_CHECK;
402 	if(ga->use_hud_color)
403 		bpp = 8;
404 	#ifdef TIMER
405 		int start_time = timer_get_fixed_seconds();
406 	#endif
407 
408 	if(ga->current_frame == ga->previous_frame)
409 		return;
410 
411 	#ifdef TIMER
412 		mprintf(("=========================\n"));
413 		mprintf(("frame: %d\n", ga->current_frame));
414 	#endif
415 
416 	anim_check_for_palette_change(ga->ani.instance);
417 	// if we're using bitmap polys
418 	BM_SELECT_TEX_FORMAT();
419 	if(ga->direction & GENERIC_ANIM_DIRECTION_BACKWARDS) {
420 		//grab the keyframe - every frame is a keyframe for ANI
421 		if(ga->ani.animation->flags & ANF_STREAMED) {
422 			ga->ani.instance->file_offset = ga->ani.animation->file_offset + ga->ani.animation->keys[ga->current_frame].offset;
423 		} else {
424 			ga->ani.instance->data = ga->ani.animation->data + ga->ani.animation->keys[ga->current_frame].offset;
425 		}
426 		if(ga->ani.animation->flags & ANF_STREAMED) {
427 			ga->ani.instance->file_offset = unpack_frame_from_file(ga->ani.instance, ga->buffer, ga->width * ga->height, (ga->ani.instance->xlate_pal) ? ga->ani.animation->palette_translation : NULL, (bpp==8)?1:0, bpp);
428 		}
429 		else {
430 			ga->ani.instance->data = unpack_frame(ga->ani.instance, ga->ani.instance->data, ga->buffer, ga->width * ga->height, (ga->ani.instance->xlate_pal) ? ga->ani.animation->palette_translation : NULL, (bpp==8)?1:0, bpp);
431 		}
432 	}
433 	else {
434 		//looping back
435 		if((ga->current_frame == 0) || (ga->current_frame < ga->previous_frame)) {
436 			//go back to keyframe if there is one
437 			if(ga->keyframe && (ga->current_frame > 0)) {
438 				if(ga->ani.animation->flags & ANF_STREAMED) {
439 					ga->ani.instance->file_offset = ga->ani.animation->file_offset + ga->keyoffset;
440 				} else {
441 					ga->ani.instance->data = ga->ani.animation->data + ga->keyoffset;
442 				}
443 				ga->previous_frame = ga->keyframe - 1;
444 			}
445 			//go back to the start
446 			else {
447 				ga->ani.instance->file_offset = ga->ani.animation->file_offset;
448 				ga->ani.instance->data = ga->ani.animation->data;
449 				ga->previous_frame = -1;
450 			}
451 		}
452 		#ifdef TIMER
453 				mprintf(("proc: %d\n", timer_get_fixed_seconds() - start_time));
454 				mprintf(("previous frame: %d\n", ga->previous_frame));
455 		#endif
456 		for(i = ga->previous_frame + 1; i <= ga->current_frame; i++) {
457 			if(ga->ani.animation->flags & ANF_STREAMED) {
458 				ga->ani.instance->file_offset = unpack_frame_from_file(ga->ani.instance, ga->buffer, ga->width * ga->height, (ga->ani.instance->xlate_pal) ? ga->ani.animation->palette_translation : NULL, (bpp==8)?1:0, bpp);
459 			}
460 			else {
461 				ga->ani.instance->data = unpack_frame(ga->ani.instance, ga->ani.instance->data, ga->buffer, ga->width * ga->height, (ga->ani.instance->xlate_pal) ? ga->ani.animation->palette_translation : NULL, (bpp==8)?1:0, bpp);
462 			}
463 		}
464 	}
465 	// always go back to screen format
466 	BM_SELECT_SCREEN_FORMAT();
467 	//we need to use this because performance is worse if we flush the gfx card buffer
468 
469 	gr_update_texture(ga->bitmap_id, bpp, ga->buffer, ga->width, ga->height);
470 
471 	//in case we want to check that the frame is actually changing
472 	//mprintf(("frame crc = %08X\n", cf_add_chksum_long(0, ga->buffer, ga->width * ga->height * (bpp >> 3))));
473 	ga->ani.instance->last_bitmap = ga->bitmap_id;
474 
475 	#ifdef TIMER
476 		mprintf(("end: %d\n", timer_get_fixed_seconds() - start_time));
477 		mprintf(("=========================\n"));
478 	#endif
479 }
480 
481 /*
482  * @brief apng specific animation rendering
483  *
484  * @param [in] ga  pointer to generic_anim struct
485  */
generic_render_png_stream(generic_anim * ga)486 void generic_render_png_stream(generic_anim* ga)
487 {
488 	if(ga->current_frame == ga->previous_frame) {
489 		return;
490 	}
491 
492 	try {
493 		if ((ga->direction & GENERIC_ANIM_DIRECTION_BACKWARDS) && (ga->previous_frame != -1)) {
494 			// mainhall door anims start backwards to ensure they stay shut
495 			// in that case (i.e. previous_frame is -1) we actually want to call
496 			// next_frame, in order to retrieve the 1st frame of the animation
497 			ga->png.anim->prev_frame();
498 		}
499 		else {
500 			ga->png.anim->next_frame();
501 		}
502 	}
503 	catch (const apng::ApngException& e) {
504 		nprintf(("apng", "Unable to get next/prev apng frame: %s\n", e.what()));
505 		return;
506 	}
507 
508 	bm_lock(ga->bitmap_id, ga->png.anim->bpp, BMP_TEX_NONCOMP, true);  // lock in 32 bpp for png
509 	int bpp = ga->png.anim->bpp;
510 	if (ga->use_hud_color) {
511 		bpp = 8;
512 	}
513 	gr_update_texture(ga->bitmap_id, bpp, ga->buffer, ga->width, ga->height);  // this will convert to 8 bpp if required
514 	bm_unlock(ga->bitmap_id);
515 }
516 
517 /*
518  * @brief calculate current frame for fixed frame delay animation formats (ani & eff)
519  *
520  * @param [in] *ga  animation data
521  * @param [in] frametime  how long this frame took
522  */
generic_anim_render_fixed_frame_delay(generic_anim * ga,float frametime)523 void generic_anim_render_fixed_frame_delay(generic_anim* ga, float frametime)
524 {
525 	float keytime = 0.0;
526 
527 	if(ga->keyframe)
528 		keytime = (ga->total_time * ((float)ga->keyframe / (float)ga->num_frames));
529 	//don't mess with the frame time if we're paused
530 	if((ga->direction & GENERIC_ANIM_DIRECTION_PAUSED) == 0) {
531 		if(ga->direction & GENERIC_ANIM_DIRECTION_BACKWARDS) {
532 			//keep going forwards if we're in a keyframe loop
533 			if(ga->keyframe && (ga->anim_time >= keytime)) {
534 				ga->anim_time += frametime;
535 				if(ga->anim_time >= ga->total_time) {
536 					ga->anim_time = keytime - 0.001f;
537 					ga->done_playing = 0;
538 				}
539 			}
540 			else {
541 				//playing backwards
542 				ga->anim_time -= frametime;
543 				if((ga->direction & GENERIC_ANIM_DIRECTION_NOLOOP) && ga->anim_time <= 0.0) {
544 					ga->anim_time = 0;		//stop on first frame when playing in reverse
545 				}
546 				else {
547 					while(ga->anim_time <= 0.0)
548 						ga->anim_time += ga->total_time;	//make sure we're always positive, so we can go back to the end
549 				}
550 			}
551 		}
552 		else {
553 			ga->anim_time += frametime;
554 			if(ga->anim_time >= ga->total_time) {
555 				if(ga->direction & GENERIC_ANIM_DIRECTION_NOLOOP) {
556 					ga->anim_time = ga->total_time - 0.001f;		//stop on last frame when playing - if it's equal we jump to the first frame
557 				}
558 				if(!ga->done_playing){
559 					//we've played this at least once
560 					ga->done_playing = 1;
561 				}
562 			}
563 		}
564 	}
565 	if(ga->num_frames > 0)
566 	{
567 		ga->current_frame = 0;
568 		if(ga->done_playing && ga->keyframe) {
569 			ga->anim_time = fmod(ga->anim_time - keytime, ga->total_time - keytime) + keytime;
570 		}
571 		else {
572 			ga->anim_time = fmod(ga->anim_time, ga->total_time);
573 		}
574 		ga->current_frame += fl2i(ga->anim_time * ga->num_frames / ga->total_time);
575 		//sanity check
576 		CLAMP(ga->current_frame, 0, ga->num_frames - 1);
577 		if(ga->streaming) {
578 			//handle streaming - render one frame
579 			if(ga->type == BM_TYPE_ANI) {
580 				generic_render_ani_stream(ga);
581 			} else {
582 				generic_render_eff_stream(ga);
583 			}
584 			gr_set_bitmap(ga->bitmap_id);
585 		}
586 		else {
587 			gr_set_bitmap(ga->first_frame + ga->current_frame);
588 		}
589 	}
590 }
591 
592 /*
593  * @brief calculate current frame for variable frame delay animation formats (e.g. apng)
594  *
595  * @param [in] *ga  animation data
596  * @param [in] frametime  how long this frame took
597  * @param [in] alpha  transparency to draw frame with (0.0 - 1.0)
598  *
599  * @note
600  * uses both time & frame counts to determine end state; so that if the anims playing
601  * can't be processed fast enough, all frames will still play, rather than the end
602  * frames being skipped
603  */
generic_anim_render_variable_frame_delay(generic_anim * ga,float frametime,float alpha)604 void generic_anim_render_variable_frame_delay(generic_anim* ga, float frametime, float alpha)
605 {
606 	Assertion(ga->type == BM_TYPE_PNG, "only valid for apngs (currently); get a coder!");
607 	if (ga->keyframe != 0) {
608 		Warning(LOCATION, "apngs don't support keyframes");
609 		return;
610 	}
611 
612 	// don't change the frame time if we're paused
613 	if((ga->direction & GENERIC_ANIM_DIRECTION_PAUSED) == 0) {
614 		if(ga->direction & GENERIC_ANIM_DIRECTION_BACKWARDS) {
615 			// playing backwards
616 			ga->anim_time -= frametime;
617 			if (ga->anim_time <= 0.0 && ga->png.anim->current_frame <= 0) {
618 				if(ga->direction & GENERIC_ANIM_DIRECTION_NOLOOP) {
619 					ga->anim_time = 0;  //stop on first frame when playing in reverse
620 				}
621 				else {
622 					// loop back to end
623 					ga->anim_time = ga->total_time;
624 					ga->png.previous_frame_time = ga->total_time;
625 					ga->png.anim->current_frame = ga->num_frames-1;
626 					ga->current_frame = ga->num_frames-1;
627 				}
628 			}
629 		}
630 		else {
631 			// playing forwards
632 			ga->anim_time += frametime;
633 			if(ga->anim_time >= ga->total_time && ga->png.anim->current_frame >= ga->png.anim->nframes) {
634 				if(ga->direction & GENERIC_ANIM_DIRECTION_NOLOOP) {
635 					ga->anim_time = ga->total_time;  // stop on last frame when playing
636 				}
637 				else {
638 					// loop back to start
639 					ga->anim_time = 0.0f;
640 					ga->png.previous_frame_time = 0.0f;
641 					ga->current_frame = 0;
642 					ga->png.anim->goto_start();
643 				}
644 				ga->done_playing = 1;
645 			}
646 		}
647 	}
648 
649 	if (ga->num_frames > 0) {
650 
651 		// just increment or decrement the frame by one
652 		// jumping forwards multiple frames will just exacerbate slowdowns as multiple frames
653 		// would need to be composed
654 		if (ga->direction & GENERIC_ANIM_DIRECTION_BACKWARDS) {
655 			if (ga->anim_time <= ga->png.previous_frame_time - ga->png.anim->frame.delay &&
656 					ga->png.anim->current_frame > 0) {
657 				ga->png.previous_frame_time -= ga->png.anim->frame.delay;
658 				ga->current_frame--;
659 			}
660 		}
661 		else {
662 			if (ga->anim_time >= ga->png.previous_frame_time + ga->png.anim->frame.delay &&
663 					ga->png.anim->current_frame < ga->png.anim->nframes) {
664 				ga->png.previous_frame_time += ga->png.anim->frame.delay;
665 				ga->current_frame++;
666 			}
667 		}
668 
669 		// verbose debug; but quite useful
670 		nprintf(("apng", "apng generic render timings/frames: %04f %04f %04f %04f | %03i %03i %03i\n",
671 				frametime, ga->anim_time, ga->png.anim->frame.delay, ga->png.previous_frame_time,
672 				ga->previous_frame, ga->current_frame, ga->png.anim->current_frame));
673 
674 		Assertion(ga->streaming != 0, "non-streaming apngs not implemented yet");
675 		// note: generic anims are not currently ever non-streaming in FSO
676 		// I'm not even sure that the existing ani/eff code would allow non-streaming generic anims
677 		generic_render_png_stream(ga);
678 		gr_set_bitmap(ga->bitmap_id, GR_ALPHABLEND_FILTER, GR_BITBLT_MODE_NORMAL, alpha);
679 	}
680 }
681 
682 
683 /*
684  * @brief render animations
685  *
686  * @param [in] *ga  animation data
687  * @param [in] frametime  how long this frame took
688  * @param [in] x  2D screen x co-ordinate to render at
689  * @param [in] y  2D screen y co-ordinate to render at
690  * @param [in] menu select if this is rendered in menu screen, or fullscreen
691  */
generic_anim_render(generic_anim * ga,float frametime,int x,int y,bool menu,const generic_extras * ge)692 void generic_anim_render(generic_anim *ga, float frametime, int x, int y, bool menu, const generic_extras *ge)
693 {
694 	if ((ge != nullptr) && (ga->use_hud_color == true)) {
695 		Warning(LOCATION, "Monochrome generic anims can't use extra info (yet)");
696 		return;
697 	}
698 
699 	float a = 1.0f;
700 	if (ge != nullptr) {
701 		a = ge->alpha;
702 	}
703 	if (ga->type == BM_TYPE_PNG) {
704 		generic_anim_render_variable_frame_delay(ga, frametime, a);
705 	}
706 	else {
707 		generic_anim_render_fixed_frame_delay(ga, frametime);
708 	}
709 
710 	if(ga->num_frames > 0) {
711 		ga->previous_frame = ga->current_frame;
712 
713 		if(ga->use_hud_color) {
714 			gr_aabitmap(x, y, (menu ? GR_RESIZE_MENU : GR_RESIZE_FULL));
715 		}
716 		else {
717 			if (ge == nullptr) {
718 				gr_bitmap(x, y, (menu ? GR_RESIZE_MENU : GR_RESIZE_FULL));
719 			}
720 			else if (ge->draw == true) {
721 				// currently only for lua streaminganim objects
722 				// and don't draw them unless requested...
723 				gr_bitmap_uv(x, y, ge->width, ge->height, ge->u0, ge->v0, ge->u1, ge->v1, GR_RESIZE_NONE);
724 			}
725 		}
726 	}
727 }
728 
729 /*
730  * @brief reset an animation back to the start
731  *
732  * @param [in] *ga  animation data
733  */
generic_anim_reset(generic_anim * ga)734 void generic_anim_reset(generic_anim *ga) {
735 	ga->anim_time = 0.0f;
736 	ga->current_frame = 0;
737 	if (ga->type == BM_TYPE_PNG) {
738 		ga->png.previous_frame_time = 0.0f;
739 		ga->png.anim->goto_start();
740 	}
741 }
742