taisei/src/resource/animation.c
2024-05-17 14:11:48 +02:00

375 lines
9.7 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "animation.h"
#include "renderer/api.h"
#include "resource.h"
#include "util/kvparser.h"
static char *animation_path(const char *name) {
return strjoin(ANI_PATH_PREFIX, name, ANI_EXTENSION, NULL);
}
static bool check_animation_path(const char *path) {
return strendswith(path, ANI_EXTENSION);
}
// See ANIMATION_FORMAT.rst for a documentation of this syntax.
static bool animation_parse_sequence_spec(AniSequence **seq, int seq_capacity, const char *specstr) {
const char *delaytoken = "d";
const char *mirrortoken = "m";
const char *p = specstr;
// skip initial whitespace
while(isspace(*p))
p++;
enum {
ANISEQ_SPRITEIDX,
ANISEQ_DELAY,
ANISEQ_MIRROR
};
int delay = 1; // standard frame duration: one frame.
bool mirrored = false;
int command = 1;
// This loop is supposed to parse one command per iteration.
while(*p != '\0') {
int state = ANISEQ_SPRITEIDX;
if(strstartswith(p,delaytoken)) {
p += strlen(delaytoken);
state = ANISEQ_DELAY;
} else if(strstartswith(p,mirrortoken)) {
p += strlen(mirrortoken);
state = ANISEQ_MIRROR;
} else if(!isdigit(*p)) {
log_error("AniSequence syntax error: '%s'[%d] does not start with a proper command",specstr,command);
return false;
}
const char *endptr = p;
int arg = 0;
if(!isspace(*p)) { // strtol will ignore leadingwhitespace and therefore eat the next sprite number if there is no arg!
char *tmp;
arg = strtol(p,&tmp,10);
endptr = tmp;
}
bool no_arg = endptr-p == 0;
if(*endptr != '\0' && !isspace(*endptr)) {
log_error("AniSequence syntax error: '%s'[%d] contains unexpected character '%c'",specstr,command,*endptr);
return false;
}
if(state != ANISEQ_MIRROR && no_arg) {
log_error("AniSequence syntax error: '%s'[%d] expected argument after command",specstr,command);
return false;
}
p = endptr;
while(isspace(*p))
p++;
// expression parsed, now react
if(state == ANISEQ_DELAY) {
if(arg < 0) {
log_error("AniSequence syntax error: '%s'[%d] delay cannot be negative.",specstr,command);
return false;
}
delay = arg;
} else if(state == ANISEQ_MIRROR) {
if(no_arg) {
mirrored = !mirrored;
} else if(arg == 0 || arg == 1) {
mirrored = arg;
} else {
log_error("AniSequence syntax error: '%s'[%d] mirror can only take 0 or 1 or nothing as an argument. %s",specstr,command,endptr);
return false;
}
} else if(state == ANISEQ_SPRITEIDX) {
if(arg < 0) {
log_error("AniSequence syntax error: '%s'[%d] sprite index cannot be negative.",specstr,command);
return false;
}
// temporarily store flipped sprites with negative indices.
// a later pass will allocate actual flipped sprites for these and fix up the indices.
int spriteidx = mirrored ? -(arg + 1) : arg;
for(int i = 0; i < delay; i++) {
if(seq_capacity <= (*seq)->length) {
seq_capacity *= 2;
*seq = mem_realloc(*seq, sizeof(AniSequence) + seq_capacity * sizeof(*(*seq)->frame_indices));
}
(*seq)->frame_indices[(*seq)->length++] = spriteidx;
}
}
command++;
}
if((*seq)->length <= 0) {
log_error("AniSequence syntax error: '%s' empty sequence.",specstr);
return false;
}
return true;
}
static bool animation_parse_callback(const char *key, const char *value, void *data) {
Animation *ani = (Animation *)data;
// parse fixed keys
if(!strcmp(key, "@sprite_count")) {
char *endptr;
ani->sprite_count = strtol(value,&endptr,10);
if(*value == '\0' || *endptr != '\0' || ani->sprite_count <= 0) {
log_error("%s must be a positive integer (got `%s`)", key, value);
return false;
}
return true;
}
if(key[0] == '@') {
log_warn("Unknown animation property `%s` ignored", key);
return true;
}
// otherwise it is a sequence
AniSequence *prev_seq;
if((prev_seq = ht_get(&ani->sequences, key, NULL))) {
log_warn("Animation sequence `%s` overwritten", key);
}
int init_seq_size = 4;
AniSequence *seq = ALLOC_FLEX(typeof(*seq), init_seq_size * sizeof(*seq->frame_indices));
if(!animation_parse_sequence_spec(&seq, init_seq_size, value)) {
mem_free(seq);
return false;
}
ht_set(&ani->sequences, key, seq);
mem_free(prev_seq);
return true;
}
static void *free_sequence_callback(const char *key, void *data, void *arg) {
mem_free(data);
return NULL;
}
static void load_animation_stage1(ResourceLoadState *st);
static void load_animation_stage2(ResourceLoadState *st);
static void load_animation_stage1(ResourceLoadState *st) {
auto ani = ALLOC(Animation);
ht_create(&ani->sequences);
if(!parse_keyvalue_file_cb(st->path, animation_parse_callback, ani)) {
ht_foreach(&ani->sequences, free_sequence_callback, NULL);
ht_destroy(&ani->sequences);
mem_free(ani);
res_load_failed(st);
return;
}
if(ani->sprite_count <= 0) {
log_error("Animation sprite count of '%s', must be positive integer", st->name);
ht_foreach(&ani->sequences, free_sequence_callback, NULL);
ht_destroy(&ani->sequences);
mem_free(ani);
res_load_failed(st);
return;
}
char buf[strlen(st->name) + sizeof(".frame0000")];
for(int i = 0; i < ani->sprite_count; ++i) {
snprintf(buf, sizeof(buf), "%s.frame%04d", st->name, i);
res_load_dependency(st, RES_SPRITE, buf);
}
res_load_continue_after_dependencies(st, load_animation_stage2, ani);
}
struct anim_remap_state {
Animation *ani;
ht_int2int_t flip_map;
int num_flipped_sprites;
int errors;
};
static void flip_sprite(Sprite *s) {
FloatRect *tex_area = &s->tex_area;
tex_area->x += tex_area->w;
tex_area->w *= -1;
}
static int remap_frame_index(struct anim_remap_state *st, int idx) {
int64_t remapped_idx;
if(idx >= 0) {
return idx;
}
if(st->num_flipped_sprites > 0 && ht_lookup(&st->flip_map, idx, &remapped_idx)) {
return remapped_idx;
}
if(st->num_flipped_sprites == 0) {
ht_create(&st->flip_map);
}
remapped_idx = st->ani->sprite_count;
int local_idx = st->num_flipped_sprites;
int orig_idx = -(idx + 1);
++st->num_flipped_sprites;
++st->ani->sprite_count;
ht_set(&st->flip_map, idx, remapped_idx);
st->ani->local_sprites = mem_realloc(st->ani->local_sprites, sizeof(*st->ani->local_sprites) * st->num_flipped_sprites);
st->ani->local_sprites[local_idx] = *st->ani->sprites[orig_idx];
flip_sprite(st->ani->local_sprites + local_idx);
log_debug("%i -> %i", orig_idx, (int)remapped_idx);
log_debug("sprite_count: %i", st->ani->sprite_count);
return remapped_idx;
}
static void *remap_sequence_callback(const char *key, void *value, void *varg) {
AniSequence *seq = value;
struct anim_remap_state *st = varg;
// needs to be cached, because we may grow the count to allocate flipped sprites
int sprite_count = st->ani->sprite_count;
int errors = 0;
for(int i = 0; i < seq->length; i++) {
int abs_idx = seq->frame_indices[i];
abs_idx = abs_idx >= 0 ? abs_idx : -(abs_idx + 1);
if(abs_idx >= sprite_count) {
log_error("Animation sequence `%s`: Sprite index %i is higher than sprite_count (%i).", key, abs_idx, sprite_count);
errors++;
}
if(seq->frame_indices[i] < 0) {
seq->frame_indices[i] = remap_frame_index(st, seq->frame_indices[i]);
}
}
if(errors) {
st->errors += errors;
return st;
}
return NULL;
}
static void unload_animation(void *vani) {
Animation *ani = vani;
ht_foreach(&ani->sequences, free_sequence_callback, NULL);
ht_destroy(&ani->sequences);
mem_free(ani->sprites);
mem_free(ani->local_sprites);
mem_free(ani);
}
static void load_animation_stage2(ResourceLoadState *st) {
Animation *ani = NOT_NULL(st->opaque);
ani->sprites = ALLOC_ARRAY(ani->sprite_count, typeof(*ani->sprites));
char buf[strlen(st->name) + sizeof(".frame0000")];
struct anim_remap_state remap_state = { 0 };
for(int i = 0; i < ani->sprite_count; ++i) {
snprintf(buf, sizeof(buf), "%s.frame%04d", st->name, i);
if(!(ani->sprites[i] = res_get_data(RES_SPRITE, buf, st->flags))) {
log_error("Animation frame '%s' not found but @sprite_count was %d",buf,ani->sprite_count);
unload_animation(ani);
ani = NULL;
goto done;
}
}
remap_state.ani = ani;
int prev_sprite_count = ani->sprite_count;
if(ht_foreach(&ani->sequences, remap_sequence_callback, &remap_state) != NULL) {
unload_animation(ani);
ani = NULL;
}
if(ani) {
if(ani->sprite_count != prev_sprite_count) {
// remapping generated new flipped sprites - add them to our sprites array
assume(ani->sprite_count > prev_sprite_count);
assume(remap_state.num_flipped_sprites == ani->sprite_count - prev_sprite_count);
assume(ani->local_sprites != NULL);
ani->sprites = mem_realloc(ani->sprites, sizeof(*ani->sprites) * ani->sprite_count);
for(int i = 0; i < remap_state.num_flipped_sprites; ++i) {
ani->sprites[prev_sprite_count + i] = ani->local_sprites + i;
}
} else {
assert(remap_state.num_flipped_sprites == 0);
assert(ani->local_sprites == NULL);
}
}
done:
if(remap_state.num_flipped_sprites > 0) {
ht_destroy(&remap_state.flip_map);
}
if(ani) {
res_load_finished(st, ani);
} else {
res_load_failed(st);
}
}
AniSequence *get_ani_sequence(Animation *ani, const char *seqname) {
AniSequence *seq = ht_get(&ani->sequences, seqname, NULL);
if(seq == NULL) {
log_fatal("Sequence '%s' not found.", seqname);
}
return seq;
}
Sprite *animation_get_frame(Animation *ani, AniSequence *seq, int seqframe) {
int idx = seq->frame_indices[seqframe % seq->length];
assert(idx < ani->sprite_count);
return ani->sprites[idx];
}
ResourceHandler animation_res_handler = {
.type = RES_ANIM,
.typename = "animation",
.subdir = ANI_PATH_PREFIX,
.procs = {
.find = animation_path,
.check = check_animation_path,
.load = load_animation_stage1,
.unload = unload_animation,
},
};