diff --git a/CMakeLists.txt b/CMakeLists.txt index 42ab0a8..b0f4a93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ target_sources(xsystem4 PRIVATE src/parts/construction.c src/parts/debug.c src/parts/flash.c + src/parts/flat.c src/parts/input.c src/parts/motion.c src/parts/parts.c diff --git a/include/parts.h b/include/parts.h index d6fe80e..2043092 100644 --- a/include/parts.h +++ b/include/parts.h @@ -238,4 +238,18 @@ bool PE_StartPartsFlash(int parts_no, int state); bool PE_GoFramePartsFlash(int parts_no, int frame_no, int state); int PE_GetPartsFlashEndFrame(int parts_no, int state); +// flat.c +bool PE_ExistsFlatFile(struct string *filename); +bool PE_SetPartsFlat(int parts_no, struct string *filename, int state); +bool PE_IsPartsFlatEnd(int parts_no, int state); +int PE_GetPartsFlatCurrentFrameNumber(int parts_no, int state); +bool PE_BackPartsFlatBeginFrame(int parts_no, int state); +bool PE_StepPartsFlatFinalFrame(int parts_no, int state); +bool PE_SetPartsFlatSurfaceArea(int parts_no, int x, int y, int w, int h, int state); +bool PE_SetPartsFlatAndStop(int parts_no, struct string *filename, int state); +bool PE_StopPartsFlat(int parts_no, int state); +bool PE_StartPartsFlat(int parts_no, int state); +bool PE_GoFramePartsFlat(int parts_no, int frame_no, int state); +int PE_GetPartsFlatEndFrame(int parts_no, int state); + #endif /* SYSTEM4_PARTS_H */ diff --git a/src/hll/PartsEngine.c b/src/hll/PartsEngine.c index b4fbb4a..cf8af87 100644 --- a/src/hll/PartsEngine.c +++ b/src/hll/PartsEngine.c @@ -563,18 +563,18 @@ HLL_LIBRARY(PartsEngine, HLL_TODO_EXPORT(Parts_SetPartsRectangleDetectionSurfaceArea, PartsEngine_Parts_SetPartsRectangleDetectionSurfaceArea), HLL_EXPORT(Parts_SetPartsCGDetectionSize, PE_SetPartsCGDetectionSize), HLL_TODO_EXPORT(Parts_SetPartsCGDetectionSurfaceArea, PartsEngine_Parts_SetPartsCGDetectionSurfaceArea), - HLL_TODO_EXPORT(Parts_SetPartsFlat, PartsEngine_Parts_SetPartsFlat), - HLL_TODO_EXPORT(Parts_IsPartsFlatEnd, PartsEngine_Parts_IsPartsFlatEnd), - HLL_TODO_EXPORT(Parts_GetPartsFlatCurrentFrameNumber, PartsEngine_Parts_GetPartsFlatCurrentFrameNumber), - HLL_TODO_EXPORT(Parts_BackPartsFlatBeginFrame, PartsEngine_Parts_BackPartsFlatBeginFrame), - HLL_TODO_EXPORT(Parts_StepPartsFlatFinalFrame, PartsEngine_Parts_StepPartsFlatFinalFrame), - HLL_TODO_EXPORT(Parts_SetPartsFlatSurfaceArea, PartsEngine_Parts_SetPartsFlatSurfaceArea), - HLL_TODO_EXPORT(Parts_SetPartsFlatAndStop, PartsEngine_Parts_SetPartsFlatAndStop), - HLL_TODO_EXPORT(Parts_StopPartsFlat, PartsEngine_Parts_StopPartsFlat), - HLL_TODO_EXPORT(Parts_StartPartsFlat, PartsEngine_Parts_StartPartsFlat), - HLL_TODO_EXPORT(Parts_GoFramePartsFlat, PartsEngine_Parts_GoFramePartsFlat), - HLL_TODO_EXPORT(Parts_GetPartsFlatEndFrame, PartsEngine_Parts_GetPartsFlatEndFrame), - HLL_TODO_EXPORT(Parts_ExistsFlatFile, PartsEngine_Parts_ExistsFlatFile), + HLL_EXPORT(Parts_SetPartsFlat, PE_SetPartsFlat), + HLL_EXPORT(Parts_IsPartsFlatEnd, PE_IsPartsFlatEnd), + HLL_EXPORT(Parts_GetPartsFlatCurrentFrameNumber, PE_GetPartsFlatCurrentFrameNumber), + HLL_EXPORT(Parts_BackPartsFlatBeginFrame, PE_BackPartsFlatBeginFrame), + HLL_EXPORT(Parts_StepPartsFlatFinalFrame, PE_StepPartsFlatFinalFrame), + HLL_EXPORT(Parts_SetPartsFlatSurfaceArea, PE_SetPartsFlatSurfaceArea), + HLL_EXPORT(Parts_SetPartsFlatAndStop, PE_SetPartsFlatAndStop), + HLL_EXPORT(Parts_StopPartsFlat, PE_StopPartsFlat), + HLL_EXPORT(Parts_StartPartsFlat, PE_StartPartsFlat), + HLL_EXPORT(Parts_GoFramePartsFlat, PE_GoFramePartsFlat), + HLL_EXPORT(Parts_GetPartsFlatEndFrame, PE_GetPartsFlatEndFrame), + HLL_EXPORT(Parts_ExistsFlatFile, PE_ExistsFlatFile), HLL_TODO_EXPORT(Parts_GetPartsFlatDataInfo, PartsEngine_Parts_GetPartsFlatDataInfo), HLL_TODO_EXPORT(Parts_ChangeFlatCG, PartsEngine_Parts_ChangeFlatCG), HLL_TODO_EXPORT(Parts_ChangeFlatSound, PartsEngine_Parts_ChangeFlatSound), diff --git a/src/meson.build b/src/meson.build index 22dda43..c2b7e04 100644 --- a/src/meson.build +++ b/src/meson.build @@ -59,6 +59,7 @@ xsystem4 = [version_h, 'parts/construction.c', 'parts/debug.c', 'parts/flash.c', + 'parts/flat.c', 'parts/input.c', 'parts/motion.c', 'parts/parts.c', diff --git a/src/parts/debug.c b/src/parts/debug.c index a123104..b4c9ae9 100644 --- a/src/parts/debug.c +++ b/src/parts/debug.c @@ -200,6 +200,15 @@ static void parts_flash_to_json(struct parts_flash *flash, cJSON *out, bool verb cJSON_AddNumberToObject(out, "current_frame", flash->current_frame); } +static void parts_flat_to_json(struct parts_flat *flat, cJSON *out, bool verbose) +{ + if (flat->name) + cJSON_AddSjisToObject(out, "name", flat->name->text); + if (flat->root_state) + cJSON_AddNumberToObject(out, "current_frame", flat->root_state->current_frame); + cJSON_AddNumberToObject(out, "end_frame", flat->end_frame); +} + static cJSON *parts_state_to_json(struct parts_state *state, bool verbose) { static const char *state_types[PARTS_NR_TYPES] = { @@ -211,7 +220,8 @@ static cJSON *parts_state_to_json(struct parts_state *state, bool verbose) [PARTS_HGAUGE] = "hgauge", [PARTS_VGAUGE] = "vgauge", [PARTS_CONSTRUCTION_PROCESS] = "construction_process", - [PARTS_FLASH] = "flash" + [PARTS_FLASH] = "flash", + [PARTS_FLAT] = "flat" }; const char *type = "invalid"; if (state->type >= 0 && state->type < PARTS_NR_TYPES) @@ -249,6 +259,9 @@ static cJSON *parts_state_to_json(struct parts_state *state, bool verbose) case PARTS_FLASH: parts_flash_to_json(&state->flash, obj, verbose); break; + case PARTS_FLAT: + parts_flat_to_json(&state->flat, obj, verbose); + break; } return obj; @@ -529,6 +542,9 @@ static void parts_list_print(struct parts *parts, int indent) case PARTS_FLASH: sys_message("(flash %s)", display_sjis0(state->flash.name->text)); break; + case PARTS_FLAT: + sys_message("(flat %s)", state->flat.name ? display_sjis0(state->flat.name->text) : "(null)"); + break; } sys_message(" @ z=%d (%d)\n", parts->global.z, parts->local.z); diff --git a/src/parts/flat.c b/src/parts/flat.c new file mode 100644 index 0000000..3609c9a --- /dev/null +++ b/src/parts/flat.c @@ -0,0 +1,475 @@ +/* Copyright (C) 2026 kichikuou + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include + +#include "system4.h" +#include "system4/archive.h" +#include "system4/cg.h" +#include "system4/flat.h" +#include "system4/string.h" + +#include "asset_manager.h" +#include "gfx/gfx.h" +#include "parts.h" +#include "parts_internal.h" +#include "xsystem4.h" + +static struct flat_layer_state *flat_layer_state_new(size_t nr_timelines) +{ + struct flat_layer_state *s = xcalloc(1, sizeof(struct flat_layer_state)); + s->suppress_advance = true; + s->jump_target = -1; + s->nr_timelines = nr_timelines; + s->last_script_keys = xmalloc(nr_timelines * sizeof(int)); + for (size_t i = 0; i < nr_timelines; i++) + s->last_script_keys[i] = -2; // uninitialized + s->children = xcalloc(nr_timelines, sizeof(struct flat_layer_state *)); + return s; +} + +static void flat_layer_state_free(struct flat_layer_state *s) +{ + if (!s) + return; + for (size_t i = 0; i < s->nr_timelines; i++) + flat_layer_state_free(s->children[i]); + free(s->children); + free(s->last_script_keys); + free(s); +} + +void parts_flat_free(struct parts_flat *f) +{ + if (f->flat) + flat_free(f->flat); + if (f->name) + free_string(f->name); + flat_layer_state_free(f->root_state); + if (f->textures) { + for (size_t i = 0; i < f->nr_textures; i++) + gfx_delete_texture(&f->textures[i]); + free(f->textures); + } +} + +int parts_flat_find_library(struct flat *fl, const char *name) +{ + for (size_t i = 0; i < fl->nr_libraries; i++) { + if (strcmp(fl->libraries[i].name->text, name) == 0) + return (int)i; + } + return -1; +} + +static int flat_compute_max_frame(struct flat_timeline *timelines, size_t nr_timelines) +{ + int max_frame = 0; + for (size_t i = 0; i < nr_timelines; i++) { + int end = timelines[i].begin_frame + timelines[i].frame_count; + if (end > max_frame) + max_frame = end; + } + return max_frame; +} + +// Process script timelines for the current frame. +static void flat_process_layer_scripts(struct flat_layer_state *state, + struct flat_timeline *timelines, size_t nr_timelines) +{ + if (state->stopped) + return; + for (size_t i = 0; i < nr_timelines; i++) { + if (timelines[i].type != FLAT_TIMELINE_SCRIPT) + continue; + + // Find last key where frame_index <= current_frame + int matched = -1; + for (size_t j = 0; j < timelines[i].script.count; j++) { + if ((int)timelines[i].script.keys[j].frame_index <= state->current_frame) + matched = (int)j; + else + break; + } + + // Only trigger when matched key differs + if (state->last_script_keys[i] == matched) + continue; + state->last_script_keys[i] = matched; + if (matched < 0) + continue; + + struct flat_script_key *key = &timelines[i].script.keys[matched]; + if (key->has_jump) { + state->jump_target = key->jump_frame; + } else if (key->is_stop) { + state->stopped = true; + } + } +} + +// Advance children regardless of this layer's stopped state. +static bool flat_advance_children(struct flat *fl, + struct flat_layer_state *state, + struct flat_timeline *timelines, size_t nr_timelines); + +// Advance a layer by one tick. Returns true if any frame changed. +static bool flat_advance_layer(struct flat *fl, + struct flat_layer_state *state, + struct flat_timeline *timelines, size_t nr_timelines) +{ + int max_end_frame = flat_compute_max_frame(timelines, nr_timelines) - 1; + int prev_frame = state->current_frame; + + do { + int next_frame; + if (state->jump_target != -1) { + next_frame = state->jump_target; + state->jump_target = -1; + } else { + int increment = (state->stopped || state->suppress_advance) ? 0 : 1; + next_frame = state->current_frame + increment; + } + + state->suppress_advance = false; + state->current_frame = min(max(0, next_frame), max_end_frame); + + flat_process_layer_scripts(state, timelines, nr_timelines); + } while (state->jump_target != -1); + + bool child_changed = flat_advance_children(fl, state, timelines, nr_timelines); + return state->current_frame != prev_frame || child_changed; +} + +static bool flat_advance_children(struct flat *fl, + struct flat_layer_state *state, + struct flat_timeline *timelines, size_t nr_timelines) +{ + bool changed = false; + for (size_t i = 0; i < nr_timelines; i++) { + if (timelines[i].type != FLAT_TIMELINE_GRAPHIC) + continue; + + // Destroy child states when the parent frame leaves the active range. + int local = state->current_frame - timelines[i].begin_frame; + if (local < 0 || local >= timelines[i].frame_count) { + if (state->children[i]) { + flat_layer_state_free(state->children[i]); + state->children[i] = NULL; + } + continue; + } + + int lib_idx = parts_flat_find_library(fl, timelines[i].library_name->text); + if (lib_idx < 0) + continue; + struct flat_library *lib = &fl->libraries[lib_idx]; + if (lib->type != FLAT_LIB_TIMELINE) + continue; + + if (!state->children[i]) + state->children[i] = flat_layer_state_new(lib->timeline.nr_timelines); + if (flat_advance_layer(fl, state->children[i], + lib->timeline.timelines, lib->timeline.nr_timelines)) { + changed = true; + } + } + return changed; +} + +bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *filename) +{ + if (f->flat) { + parts_flat_free(f); + memset(f, 0, sizeof(struct parts_flat)); + } + + if (!filename) + return false; + + struct archive_data *dfile = asset_get_by_name(ASSET_FLAT, filename->text, NULL); + if (!dfile) + return false; + + int error; + f->flat = flat_open(dfile->data, dfile->size, &error); + if (!f->flat) { + archive_free_data(dfile); + return false; + } + + f->name = string_dup(filename); + f->stopped = false; + f->elapsed = 0; + f->end_frame = flat_compute_max_frame( + f->flat->timelines, f->flat->nr_timelines) - 1; + f->root_state = flat_layer_state_new(f->flat->nr_timelines); + + // Load textures for CG libraries. + f->nr_textures = f->flat->nr_libraries; + f->textures = xcalloc(f->nr_textures, sizeof(Texture)); + for (size_t i = 0; i < f->flat->nr_libraries; i++) { + struct flat_library *lib = &f->flat->libraries[i]; + if (lib->type != FLAT_LIB_CG) + continue; + struct cg *cg = cg_load_buffer((uint8_t *)lib->cg.data, lib->cg.size); + if (!cg) { + WARNING("flat: failed to load CG for library '%s'", lib->name->text); + continue; + } + gfx_init_texture_with_cg(&f->textures[i], cg); + cg_free(cg); + } + + // TODO: The original engine performs per-frame hit testing against + // the flat's visible sprites (with full transform chain), rather than + // using a static hitbox. For now, estimate dimensions from CG textures. + int w = f->flat->hdr.width; + int h = f->flat->hdr.height; + if (w <= 0 || h <= 0) { + w = 0; + h = 0; + for (size_t i = 0; i < f->nr_textures; i++) { + if (f->textures[i].w > w) + w = f->textures[i].w; + if (f->textures[i].h > h) + h = f->textures[i].h; + } + } + if (w > 0 && h > 0) + parts_set_dims(parts, &f->common, w, h); + + // f->flat contains pointers into dfile->data, but it should be safe to + // free now since we've loaded all CG data we need. + archive_free_data(dfile); + return true; +} + +bool parts_flat_update(struct parts_flat *f, int passed_time) +{ + if (!f->flat || f->flat->hdr.fps <= 0) + return false; + + // Consume pending forward seek (from GoFramePartsFlat). + if (f->pending_seek_delta > 0) { + for (int i = 0; i < f->pending_seek_delta; i++) { + flat_advance_layer(f->flat, f->root_state, + f->flat->timelines, f->flat->nr_timelines); + } + f->pending_seek_delta = 0; + f->elapsed = 0; + return true; + } + + // One advance after load / seek. + if (f->needs_advance) { + f->needs_advance = false; + bool changed = flat_advance_layer(f->flat, f->root_state, + f->flat->timelines, f->flat->nr_timelines); + f->elapsed = 0; + return changed; + } + + if (f->stopped) + return false; + + f->elapsed += passed_time; + int delta = (int)((int64_t)f->elapsed * f->flat->hdr.fps / 1000); + if (!delta) + return false; + f->elapsed -= (unsigned)((int64_t)delta * 1000 / f->flat->hdr.fps); + + bool any_changed = false; + for (int i = 0; i < delta; i++) { + if (flat_advance_layer(f->flat, f->root_state, + f->flat->timelines, f->flat->nr_timelines)) + any_changed = true; + } + return any_changed; +} + +bool PE_ExistsFlatFile(struct string *filename) +{ + if (!filename) + return false; + return asset_exists_by_name(ASSET_FLAT, filename->text, NULL); +} + +bool PE_SetPartsFlat(int parts_no, struct string *filename, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (parts_flat_load(parts, f, filename)) { + f->needs_advance = true; + parts_dirty(parts); + return true; + } + return false; +} + +bool PE_IsPartsFlatEnd(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat || !f->root_state) + return true; + return f->root_state->current_frame == f->end_frame; +} + +int PE_GetPartsFlatCurrentFrameNumber(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return 0; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat || !f->root_state) + return 0; + return f->root_state->current_frame + f->pending_seek_delta; +} + +bool PE_BackPartsFlatBeginFrame(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat) + return false; + f->pending_seek_delta = 0; + flat_layer_state_free(f->root_state); + f->root_state = flat_layer_state_new(f->flat->nr_timelines); + f->needs_advance = true; + f->elapsed = 0; + parts_dirty(parts); + return true; +} + +bool PE_StepPartsFlatFinalFrame(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat || !f->root_state) + return false; + + // Directly set to end frame. + f->root_state->current_frame = f->end_frame; + f->root_state->stopped = false; + f->root_state->suppress_advance = true; + f->pending_seek_delta = 0; + f->needs_advance = true; + f->elapsed = 0; + parts_dirty(parts); + return true; +} + +bool PE_SetPartsFlatSurfaceArea(int parts_no, int x, int y, int w, int h, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + parts_set_surface_area(parts, &f->common, x, y, w, h); + return true; +} + +bool PE_SetPartsFlatAndStop(int parts_no, struct string *filename, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + bool result = parts_flat_load(parts, f, filename); + f->stopped = true; + return result; +} + +bool PE_StopPartsFlat(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat) + return false; + f->stopped = true; + return true; +} + +bool PE_StartPartsFlat(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat) + return false; + f->stopped = false; + return true; +} + +bool PE_GoFramePartsFlat(int parts_no, int frame_no, int state) +{ + if (!parts_state_valid(--state)) + return false; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + if (!f->flat || !f->root_state) + return false; + + int current = f->root_state->current_frame; + if (frame_no < current) { + // Backward seek: directly set current_frame. + f->root_state->current_frame = min(max(0, frame_no), f->end_frame); + f->root_state->stopped = false; + f->root_state->suppress_advance = true; + f->pending_seek_delta = 0; + f->needs_advance = true; + } else { + // Forward seek: defer to next update. + f->pending_seek_delta = frame_no - current; + } + f->elapsed = 0; + parts_dirty(parts); + return true; +} + +int PE_GetPartsFlatEndFrame(int parts_no, int state) +{ + if (!parts_state_valid(--state)) + return 0; + + struct parts *parts = parts_get(parts_no); + struct parts_flat *f = parts_get_flat(parts, state); + return f->flat ? f->end_frame : -1; +} diff --git a/src/parts/parts.c b/src/parts/parts.c index 4e33c48..9575acc 100644 --- a/src/parts/parts.c +++ b/src/parts/parts.c @@ -175,6 +175,9 @@ static void parts_state_free(struct parts_state *state) case PARTS_FLASH: parts_flash_free(&state->flash); break; + case PARTS_FLAT: + parts_flat_free(&state->flat); + break; } memset(state, 0, sizeof(struct parts_state)); } @@ -220,6 +223,7 @@ void parts_state_reset(struct parts_state *state, enum parts_type type) case PARTS_CG: case PARTS_ANIMATION: case PARTS_FLASH: + case PARTS_FLAT: break; } } @@ -288,6 +292,14 @@ struct parts_flash *parts_get_flash(struct parts *parts, int state) return &parts->states[state].flash; } +struct parts_flat *parts_get_flat(struct parts *parts, int state) +{ + if (parts->states[state].type != PARTS_FLAT) { + parts_state_reset(&parts->states[state], PARTS_FLAT); + } + return &parts->states[state].flat; +} + static Point calculate_offset(int mode, int w, int h) { switch (mode) { @@ -838,20 +850,10 @@ void parts_set_surface_area(struct parts *parts, struct parts_common *common, in parts_common_recalculate_hitbox(parts, common); } -static void parts_update_loop(struct parts *parts, int passed_time) +static bool parts_animation_update(struct parts_animation *anim, int passed_time) { - if (parts->states[parts->state].type == PARTS_FLASH) { - if (parts_flash_update(&parts->states[parts->state].flash, passed_time)) - parts_dirty(parts); - return; - } - - if (parts->states[parts->state].type != PARTS_ANIMATION) - return; - - struct parts_animation *anim = &parts->states[parts->state].anim; if (passed_time <= 0 || !anim->nr_frames) - return; + return false; const unsigned elapsed = anim->elapsed + passed_time; const unsigned frame_diff = elapsed / anim->frame_time; @@ -861,12 +863,33 @@ static void parts_update_loop(struct parts *parts, int passed_time) anim->elapsed = remainder; anim->current_frame = (anim->current_frame + frame_diff) % anim->nr_frames; anim->common.texture = anim->frames[anim->current_frame]; - parts_dirty(parts); + return true; } else { anim->elapsed = elapsed; + return false; } } +static void parts_update_loop(struct parts *parts, int passed_time) +{ + bool dirty = false; + switch (parts->states[parts->state].type) { + case PARTS_ANIMATION: + dirty = parts_animation_update(&parts->states[parts->state].anim, passed_time); + break; + case PARTS_FLASH: + dirty = parts_flash_update(&parts->states[parts->state].flash, passed_time); + break; + case PARTS_FLAT: + dirty = parts_flat_update(&parts->states[parts->state].flat, passed_time); + break; + default: + break; + } + if (dirty) + parts_dirty(parts); +} + static void parts_update_animation(int passed_time) { struct parts *parts; diff --git a/src/parts/parts_internal.h b/src/parts/parts_internal.h index 8cf347f..7078389 100644 --- a/src/parts/parts_internal.h +++ b/src/parts/parts_internal.h @@ -103,7 +103,8 @@ enum parts_type { PARTS_VGAUGE, PARTS_CONSTRUCTION_PROCESS, PARTS_FLASH, -#define PARTS_NR_TYPES (PARTS_FLASH+1) + PARTS_FLAT, +#define PARTS_NR_TYPES (PARTS_FLAT+1) }; struct parts_common { @@ -285,6 +286,33 @@ struct parts_flash { TAILQ_HEAD(, parts_flash_object) display_list; }; +struct flat_layer_state { + int current_frame; + bool stopped; + bool suppress_advance; + int jump_target; // pending jump frame, -1 = none + // Per-timeline: last matched script key index for change detection + // (-2 = uninitialized, -1 = no match, >= 0 = key index) + int *last_script_keys; + // Per-timeline: child layer state (for TIMELINE libs), NULL otherwise + struct flat_layer_state **children; + size_t nr_timelines; +}; + +struct parts_flat { + struct parts_common common; + struct string *name; + struct flat *flat; + bool stopped; + bool needs_advance; + unsigned elapsed; + int end_frame; + int pending_seek_delta; + struct flat_layer_state *root_state; + Texture *textures; // indexed by library index (only CG libs have valid textures) + size_t nr_textures; +}; + struct parts_state { enum parts_type type; union { @@ -296,6 +324,7 @@ struct parts_state { struct parts_gauge gauge; struct parts_construction_process cproc; struct parts_flash flash; + struct parts_flat flat; }; }; @@ -355,6 +384,7 @@ struct parts_gauge *parts_get_hgauge(struct parts *parts, int state); struct parts_gauge *parts_get_vgauge(struct parts *parts, int state); struct parts_construction_process *parts_get_construction_process(struct parts *parts, int state); struct parts_flash *parts_get_flash(struct parts *parts, int state); +struct parts_flat *parts_get_flat(struct parts *parts, int state); void parts_set_pos(struct parts *parts, Point pos); void parts_set_global_pos(Point pos); void parts_set_dims(struct parts *parts, struct parts_common *common, int w, int h); @@ -426,6 +456,12 @@ bool parts_flash_load(struct parts *parts, struct parts_flash *f, struct string bool parts_flash_update(struct parts_flash *f, int passed_time); bool parts_flash_seek(struct parts_flash *f, int frame); +// flat.c +void parts_flat_free(struct parts_flat *f); +bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *filename); +bool parts_flat_update(struct parts_flat *f, int passed_time); +int parts_flat_find_library(struct flat *fl, const char *name); + // debug.c struct sprite; void parts_debug_init(void); diff --git a/src/parts/render.c b/src/parts/render.c index f8859c3..078a3bf 100644 --- a/src/parts/render.c +++ b/src/parts/render.c @@ -15,11 +15,14 @@ */ #include +#include #include #include #include "system4.h" #include "system4/hashtable.h" +#include "system4/string.h" +#include "system4/flat.h" #include "gfx/gfx.h" #include "scene.h" @@ -166,6 +169,130 @@ static void parts_render_cg(struct parts *parts, struct parts_common *common) glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); } +static void render_flat_layer(struct parts *parts, struct parts_flat *f, + struct flat_layer_state *state, + struct flat_timeline *timelines, size_t nr_timelines, + mat4 parent, float parent_alpha); + +static void render_flat_item(struct parts *parts, struct parts_flat *f, + struct flat_layer_state *state, size_t tl_idx, + struct flat_timeline *tl, + struct flat_key_data_graphic *key, + mat4 parent, float parent_alpha) +{ + int lib_idx = parts_flat_find_library(f->flat, tl->library_name->text); + if (lib_idx < 0 || (size_t)lib_idx >= f->flat->nr_libraries) + return; + + struct flat_library *lib = &f->flat->libraries[lib_idx]; + + float pos_x = (f->flat->hdr.version > 4) ? key->pos_x.f : (float)key->pos_x.i; + float pos_y = (f->flat->hdr.version > 4) ? key->pos_y.f : (float)key->pos_y.i; + + mat4 layer_m = GLM_MAT4_IDENTITY_INIT; + glm_translate(layer_m, (vec3){ pos_x, pos_y, 0 }); + glm_rotate_z(layer_m, deg2rad(key->angle_z), layer_m); + glm_scale(layer_m, (vec3){ key->scale_x, key->scale_y, 1.0f }); + glm_translate(layer_m, (vec3){ -(float)key->origin_x, -(float)key->origin_y, 0 }); + + mat4 combined; + glm_mat4_mul(parent, layer_m, combined); + + float alpha = parent_alpha * key->alpha / 255.0f; + + switch (lib->type) { + case FLAT_LIB_CG: { + if ((size_t)lib_idx >= f->nr_textures || !f->textures[lib_idx].handle) + return; + Texture *tex = &f->textures[lib_idx]; + + switch (key->draw_filter) { + case 1: // additive + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ZERO, GL_ONE); + break; + case 2: // multiply + glBlendFuncSeparate(GL_DST_COLOR, GL_ZERO, GL_ZERO, GL_ONE); + break; + case 3: // screen + glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ZERO, GL_ONE); + break; + default: + break; + } + + mat4 render_m; + glm_mat4_copy(combined, render_m); + glm_scale(render_m, (vec3){ tex->w, tex->h, 1.0f }); + + Rectangle rect; + if (key->area_width && key->area_height) { + rect = (Rectangle){ key->area_x, key->area_y, key->area_width, key->area_height }; + } else { + rect = (Rectangle){ 0, 0, tex->w, tex->h }; + } + + vec3 add_color = { key->add_r / 255.0f, key->add_g / 255.0f, key->add_b / 255.0f }; + vec3 mul_color = { key->mul_r / 255.0f, key->mul_g / 255.0f, key->mul_b / 255.0f }; + parts_render_texture(tex, render_m, &rect, alpha, add_color, mul_color, + parts->alpha_clipper_parts_no); + + if (key->draw_filter != 0) + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + break; + } + case FLAT_LIB_TIMELINE: { + struct flat_layer_state *child = state->children[tl_idx]; + if (child) { + render_flat_layer(parts, f, child, + lib->timeline.timelines, + lib->timeline.nr_timelines, + combined, alpha); + } + break; + } + // TODO: support FLAT_LIB_STOP_MOTION and FLAT_LIB_EMITTER + default: + break; + } +} + +static void render_flat_layer(struct parts *parts, struct parts_flat *f, + struct flat_layer_state *state, + struct flat_timeline *timelines, size_t nr_timelines, + mat4 parent, float parent_alpha) +{ + // reverse order for correct z-ordering + for (size_t i = nr_timelines; i-- > 0;) { + struct flat_timeline *tl = &timelines[i]; + if (tl->type != FLAT_TIMELINE_GRAPHIC) + continue; + int local = state->current_frame - tl->begin_frame; + if (local < 0 || local >= tl->frame_count) + continue; + + if (local >= (int)tl->graphic.count) + continue; + struct flat_key_data_graphic *key = &tl->graphic.keys[local]; + + render_flat_item(parts, f, state, i, tl, key, parent, parent_alpha); + } +} + +static void parts_render_flat(struct parts *parts, struct parts_flat *f) +{ + if (!f->flat || !f->root_state) + return; + + mat4 base = GLM_MAT4_IDENTITY_INIT; + glm_translate(base, (vec3){ parts->global.pos.x, parts->global.pos.y, 0 }); + glm_rotate_z(base, deg2rad(parts->local.rotation.z), base); + glm_scale(base, (vec3){ parts->global.scale.x, parts->global.scale.y, 1.0f }); + + render_flat_layer(parts, f, f->root_state, + f->flat->timelines, f->flat->nr_timelines, + base, parts->global.alpha / 255.0f); +} + static void parts_render_flash_shape(struct parts *parts, struct parts_flash *f, struct parts_flash_object *obj, struct swf_tag_define_shape *tag) { struct texture *src = ht_get_int(f->bitmaps, tag->fill_style.bitmap_id, NULL); @@ -301,6 +428,9 @@ void parts_render(struct parts *parts) case PARTS_FLASH: parts_render_flash(parts, &state->flash); break; + case PARTS_FLAT: + parts_render_flat(parts, &state->flat); + break; } } diff --git a/src/parts/save.c b/src/parts/save.c index c94d4a1..f99c00b 100644 --- a/src/parts/save.c +++ b/src/parts/save.c @@ -336,6 +336,20 @@ static void load_parts_flash(struct iarray_reader *r, struct parts *parts, parts_flash_seek(flash, iarray_read(r)); } +static void save_parts_flat(struct iarray_writer *w, struct parts_flat *flat) +{ + iarray_write_string_or_null(w, flat->name); +} + +static void load_parts_flat(struct iarray_reader *r, struct parts *parts, + struct parts_flat *flat) +{ + struct string *name = iarray_read_string_or_null(r); + parts_flat_load(parts, flat, name); + free_string(name); + flat->needs_advance = true; +} + static void save_parts_state(struct iarray_writer *w, struct parts_state *state) { iarray_write(w, state->type); @@ -369,6 +383,9 @@ static void save_parts_state(struct iarray_writer *w, struct parts_state *state) case PARTS_FLASH: save_parts_flash(w, &state->flash); break; + case PARTS_FLAT: + save_parts_flat(w, &state->flat); + break; } } @@ -408,6 +425,9 @@ static void load_parts_state(struct iarray_reader *r, struct parts *parts, case PARTS_FLASH: load_parts_flash(r, parts, &state->flash); break; + case PARTS_FLAT: + load_parts_flat(r, parts, &state->flat); + break; } }