/** * @file character.c * @brief Implements the character subsystem. */ #include #include #include #include #include #include #include #include #include #include #include #include #include akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL akgl_Character reference"); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "NULL name string pointer"); memset(obj, 0x00, sizeof(akgl_Character)); strncpy(obj->name, name, AKGL_CHARACTER_MAX_NAME_LENGTH); obj->state_sprites = SDL_CreateProperties(); FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map"); obj->sprite_add = &akgl_character_sprite_add; obj->sprite_get = &akgl_character_sprite_get; FAIL_ZERO_RETURN( errctx, SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj), AKERR_KEY, "Unable to add character to registry"); obj->refcount += 1; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state) { PREPARE_ERROR(errctx); char stateval[32]; akgl_Sprite *displaced = NULL; FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference"); FAIL_ZERO_RETURN(errctx, ref, AKERR_NULLPOINTER, "NULL sprite reference"); memset(&stateval, 0x00, 32); SDL_itoa(state, (char *)&stateval, 10); // Whatever this state was bound to is losing its binding, and the reference // taken for that binding with it. Without this a character that rebinds a // state while alive leaks one sprite slot per rebind -- releasing at // teardown only gives back whatever the map happens to hold at the end. displaced = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL); // Take the new reference before anything can fail, so a failed write cannot // leave the sprite bound with no reference behind it. ref->refcount += 1; if ( SDL_SetPointerProperty(basechar->state_sprites, (char *)&stateval, ref) == false ) { ref->refcount -= 1; FAIL_RETURN( errctx, AKERR_KEY, "Unable to bind sprite %s to character %s for state %d: %s", (char *)&ref->name, (char *)&basechar->name, state, SDL_GetError()); } if ( (displaced != NULL) && (displaced != ref) ) { PASS(errctx, akgl_heap_release_sprite(displaced)); } SDL_Log("Added sprite %s to character %s for state %b", (char *)&ref->name, (char *)&basechar->name, state); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest) { PREPARE_ERROR(errctx); char stateval[32]; FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer to sprite pointer (**dest)"); FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference"); memset(&stateval, 0x00, 32); SDL_itoa(state, (char *)&stateval, 10); *dest = (akgl_Sprite *)SDL_GetPointerProperty(basechar->state_sprites, (char *)&stateval, NULL); FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "Sprite for state %d (%b) not found in the character's registry", state, state); SUCCEED_RETURN(errctx); } // SDL iterator so we can't return error information here, void only // this means we don't have anywhere to send exceptions up to, so if we hit an error, we log and exit(1) here void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name) { PREPARE_ERROR(errctx); akgl_Sprite *spriteptr; akgl_Iterator *opflags = (akgl_Iterator *)userdata; ATTEMPT { FAIL_ZERO_BREAK(errctx, opflags, AKERR_NULLPOINTER, "Character state sprite iterator received null iterator op pointer"); FAIL_ZERO_BREAK(errctx, name, AKERR_NULLPOINTER, "Character state sprite iterator received null sprite name"); spriteptr = (akgl_Sprite *)SDL_GetPointerProperty(props, name, NULL); FAIL_ZERO_BREAK(errctx, spriteptr, AKERR_NULLPOINTER, "Character state sprite for %s not found", name); if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_RELEASE) ) { CATCH(errctx, akgl_heap_release_sprite(spriteptr)); } } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); } /** * @brief OR a JSON array of actor-state names together into one bitmask. * * This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather * than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS * and OR-ed in. A name that is not a known state is an error rather than being * skipped -- a typo in a state name would otherwise bind a sprite to the wrong * combination and show up as art that never appears. * * @param states JSON array of state-name strings. Required. An empty array is * legal and leaves @p dest as it was. * @param dest Receives the OR of every named bit. Required in practice, and * **not** checked -- the guard that should test it tests @p states * a second time instead, so a `NULL` here is a crash. It is OR-ed * into rather than assigned, so the caller must zero it first. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p states is `NULL`. * @throws AKERR_KEY If a name is not in the actor-state registry. The message * quotes it. Note that bits 11 and 12 are unreachable by name; see * akgl_registry_init_actor_state_strings. * @throws AKERR_TYPE If an array element is not a string. * @throws AKERR_OUTOFBOUNDS If the array is indexed past its end. * @throws AKGL_ERR_HEAP If the string pool is exhausted. */ static akerr_ErrorContext *character_load_json_state_int_from_strings(json_t *states, int *dest) { int i = 0; long newstate = 0; akgl_String *tmpstring = NULL; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array"); FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer"); ATTEMPT { CATCH(errctx, akgl_heap_next_string(&tmpstring)); for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) { CATCH(errctx, akgl_get_json_array_index_string(states, i, &tmpstring)); newstate = (long)SDL_GetNumberProperty(AKGL_REGISTRY_ACTOR_STATE_STRINGS, (char *)&tmpstring->data, 0); FAIL_ZERO_BREAK(errctx, newstate, AKERR_KEY, "Unknown actor state %s", (char *)&tmpstring->data); *dest = (*dest | (int)(newstate)); } } CLEANUP { IGNORE(akgl_heap_release_string(tmpstring)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief Name a character and build its state-to-sprite map from a JSON document. * * The half of akgl_character_load_json that deals with identity and sprites; the * caller handles the numeric fields afterwards. Reads `name`, initializes the * character with it, then walks `sprite_mappings` binding each named sprite to * the bitmask its `state` array adds up to. * * Every sprite named must already be in #AKGL_REGISTRY_SPRITE. * * @param json The parsed character document. Required in practice; not checked, * and passed straight to accessors that report it. * @param obj The pooled character to fill in. Required, unchecked -- it is * handed to akgl_character_initialize, which does check it. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered. * The message names the character, the state, and the sprite. * @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or * `state` is absent, or if a state name is unknown. * @throws AKERR_TYPE If one of those keys has the wrong JSON type. * @throws AKERR_OUTOFBOUNDS If an array is indexed past its end. * @throws AKGL_ERR_HEAP If the string pool is exhausted. */ static akerr_ErrorContext *character_load_json_inner(json_t *json, akgl_Character *obj) { PREPARE_ERROR(errctx); json_t *mappings = NULL; json_t *curmapping = NULL; json_t *statearray = NULL; akgl_Sprite *spriteptr = NULL; int i = 0; akgl_String *tmpstr = NULL; akgl_String *tmpstr2 = NULL; int stateval = 0; ATTEMPT { CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr)); CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data)); CATCH(errctx, akgl_get_json_array_value((json_t *)json, "sprite_mappings", &mappings)); for ( i = 0; i < json_array_size((json_t *)mappings) ; i++ ) { stateval = 0; CATCH(errctx, akgl_get_json_array_index_object((json_t *)mappings, i, &curmapping)); CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr)); spriteptr = (akgl_Sprite *)SDL_GetPointerProperty( AKGL_REGISTRY_SPRITE, tmpstr->data, NULL ); CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2)); CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray)); CATCH(errctx, character_load_json_state_int_from_strings(statearray, &stateval)); CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr)); FAIL_ZERO_BREAK( errctx, spriteptr, AKERR_NULLPOINTER, "Character %s for state %b references sprite %s but not found in the registry", tmpstr2->data, stateval, tmpstr->data ); CATCH(errctx, akgl_character_sprite_add((akgl_Character *)obj, (akgl_Sprite *)spriteptr, stateval)); } } CLEANUP { if ( tmpstr != NULL ) { IGNORE(akgl_heap_release_string(tmpstr)); } if ( tmpstr2 != NULL ) { IGNORE(akgl_heap_release_string(tmpstr2)); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_character_load_json(char *filename) { PREPARE_ERROR(errctx); json_t *json = NULL; json_error_t error; akgl_Character *obj = NULL; //akgl_String *tmpstr = NULL; FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename"); ATTEMPT { CATCH(errctx, akgl_heap_next_character(&obj)); //CATCH(errctx, akgl_heap_next_string(&tmpstr)); //CATCH(errctx, akgl_string_initialize(tmpstr, NULL)); json = (json_t *)json_load_file(filename, 0, &error); FAIL_ZERO_BREAK( errctx, json, AKERR_NULLPOINTER, "Error while loading character from %s on line %d: %s", filename, error.line, error.text ); CATCH(errctx, character_load_json_inner(json, obj)); CATCH(errctx, akgl_get_json_integer_value(json, "speedtime", (int *)&obj->speedtime)); obj->speedtime = obj->speedtime * AKGL_TIME_ONEMS_NS; CATCH(errctx, akgl_get_json_number_value(json, "speed_x", &obj->sx)); CATCH(errctx, akgl_get_json_number_value(json, "speed_y", &obj->sy)); CATCH(errctx, akgl_get_json_number_value(json, "acceleration_x", &obj->ax)); CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay)); } CLEANUP { //IGNORE(akgl_heap_release_string(tmpstr)); // The character keeps nothing that points into the document -- every // field above is copied out of it -- so the whole parsed tree goes back // here, on the success path as well as the failure one. if ( json != NULL ) { json_decref(json); json = NULL; } if ( errctx != NULL ) { if ( errctx->status != 0 ) { IGNORE(akgl_heap_release_character(obj)); } } } PROCESS(errctx) { } FINISH(errctx, true); SDL_Log("Character %s loaded from %s", obj->name, filename); SUCCEED_RETURN(errctx); }