From c540eb97961830880fe729bcf37259dae09b843b Mon Sep 17 00:00:00 2001 From: kichikuou Date: Fri, 20 Mar 2026 10:57:52 +0900 Subject: [PATCH] PartsEngine: Implement controller (layer) system PartsEngine in Rance 9 manages multiple "controllers" (called "layers" in game scripts) that provide part grouping, draw-order control, and bulk release. This implements the core controller API: - AddController / RemoveController: push/pop a controller on the stack. RemoveController releases all parts belonging to the removed controller and returns their IDs in erase_number_list. - SetActiveLayer / GetActiveLayer / GetSystemOverlayLayer: control which controller newly created parts are assigned to. The system overlay controller is a special controller that always renders on top and survives ReleaseAllWithoutSystem. - ReleaseAllWithoutSystem: releases all parts except those in the system overlay controller and re-initializes the controller stack. Controllers are identified by their position in the stack (0 = bottom). Draw ordering uses the scene's two-level sprite sorting: sp.z is set to the controller's stack index and sp.z2 to the part's Z value, so parts in higher controllers always render above lower ones regardless of individual Z values. When the parts_multi_controller flag (see below) is false, the original Z ordering is preserved (allowing Z-interleaving with non-parts scene sprites -- see #180) for compatibility with games that do not use controllers. Save/load of the controller stack and per-part controller_no is gated on a parts_multi_controller flag (set at PreLink time when the game's HLL exports AddController) rather than a save version bump. This keeps the save format stable for games that do not use controllers, while allowing the save format to evolve freely during development. --- include/parts.h | 7 ++ src/hll/PartsEngine.c | 21 ++++- src/parts/debug.c | 4 +- src/parts/parts.c | 155 +++++++++++++++++++++++++++++++++++-- src/parts/parts_internal.h | 14 ++++ src/parts/save.c | 22 ++++++ 6 files changed, 214 insertions(+), 9 deletions(-) diff --git a/include/parts.h b/include/parts.h index d83999f..3e515d4 100644 --- a/include/parts.h +++ b/include/parts.h @@ -23,6 +23,7 @@ struct page; struct string; // parts.c +void PE_enable_multi_controller(void); bool PE_Init(void); void PE_Reset(void); void PE_Update(int passed_time, bool message_window_show); @@ -67,6 +68,7 @@ bool PE_SetNumeralSurfaceArea(int parts_no, int x, int y, int w, int h, int stat void PE_ReleaseParts(int parts_no); void PE_ReleaseAllParts(void); void PE_ReleaseAllPartsWithoutSystem(void); +void PE_ReleaseAllWithoutSystem(struct page **erase_number_list); void PE_SetPos(int parts_no, int x, int y); int PE_GetPartsX(int parts_no); int PE_GetPartsY(int parts_no); @@ -113,10 +115,15 @@ bool PE_SetPartsCGDetectionSize(int parts_no, struct string *cg_name, int state) bool PE_Save(struct page **buffer); bool PE_SaveWithoutHideParts(struct page **buffer); bool PE_Load(struct page **buffer); +int PE_AddController(int index); +void PE_RemoveController(struct page **erase_number_list, int index); // GUIEngine int PE_GetFreeNumber(void); bool PE_IsExist(int parts_no); // PartsFunc interface +void PE_set_active_controller(int controller_no); +int PE_get_active_controller(void); +int PE_get_system_controller(void); bool PE_init_parts_movie(int parts_no, int width, int height, int bg_r, int bg_g, int bg_b, int state); int PE_get_movie_sprite(int parts_no, int state); diff --git a/src/hll/PartsEngine.c b/src/hll/PartsEngine.c index 239dcec..408aa87 100644 --- a/src/hll/PartsEngine.c +++ b/src/hll/PartsEngine.c @@ -115,6 +115,18 @@ static int PartsEngine_PartsFunc(int func_id, struct page **array_int, if (nr_ints != (n)) VM_ERROR("Invalid arguments for PartsFunc %d: expected %d ints, got %d", func_id, (n), nr_ints) switch (func_id) { + case 0: // void SetActiveLayer(int layer) + REQUIRE_INTS(1); + PE_set_active_controller(ints[0].i); + return 1; + case 1: // int GetActiveLayer() + REQUIRE_INTS(1); + ints[0].i = PE_get_active_controller(); + return 1; + case 2: // int GetSystemOverlayLayer() + REQUIRE_INTS(1); + ints[0].i = PE_get_system_controller(); + return 1; case 162: // bool InitPartsMovie(int parts_no, int width, int height, int bg_r, int bg_g, int bg_b, int state) REQUIRE_INTS(8); ints[7].i = PE_init_parts_movie(ints[0].i, ints[1].i, ints[2].i, ints[3].i, ints[4].i, ints[5].i, ints[6].i); @@ -307,11 +319,11 @@ HLL_LIBRARY(PartsEngine, HLL_EXPORT(PartsFunc, PartsEngine_PartsFunc), HLL_EXPORT(Release, PE_ReleaseParts), HLL_TODO_EXPORT(ReleaseAll, PartsEngine_ReleaseAll), - HLL_TODO_EXPORT(ReleaseAllWithoutSystem, PartsEngine_ReleaseAllWithoutSystem), + HLL_EXPORT(ReleaseAllWithoutSystem, PE_ReleaseAllWithoutSystem), HLL_EXPORT(GetFreeNumber, PE_GetFreeNumber), HLL_EXPORT(IsExist, PE_IsExist), - HLL_TODO_EXPORT(AddController, PartsEngine_AddController), - HLL_TODO_EXPORT(RemoveController, PartsEngine_RemoveController), + HLL_EXPORT(AddController, PE_AddController), + HLL_EXPORT(RemoveController, PE_RemoveController), HLL_EXPORT(UpdateComponent, PartsEngine_Update), HLL_EXPORT(Parts_SetThumbnailReductionSize, PE_SetThumbnailReductionSize), HLL_EXPORT(Parts_SetThumbnailMode, PE_SetThumbnailMode), @@ -684,4 +696,7 @@ static void PartsEngine_PreLink(void) static_library_replace(&lib_PartsEngine, "Update", PartsEngine_Update_Pascha3PC); } + if (get_fun(libno, "AddController")) { + PE_enable_multi_controller(); + } } diff --git a/src/parts/debug.c b/src/parts/debug.c index 7dac020..c97e504 100644 --- a/src/parts/debug.c +++ b/src/parts/debug.c @@ -380,6 +380,8 @@ cJSON *parts_to_json(struct parts *parts, bool recursive, bool verbose) cJSON_AddItemToObjectCS(obj, "clicked", parts_state_to_json(&parts->states[PARTS_STATE_CLICKED], verbose)); cJSON_AddItemToObjectCS(obj, "local", parts_params_to_json(&parts->local, verbose)); cJSON_AddItemToObjectCS(obj, "global", parts_params_to_json(&parts->global, verbose)); + if (parts_multi_controller) + cJSON_AddNumberToObject(obj, "controller", parts->controller_no); if (parts->delegate_index >= 0) cJSON_AddNumberToObject(obj, "delegate_index", parts->delegate_index); cJSON_AddNumberToObject(obj, "sprite_deform", parts->sprite_deform); @@ -558,7 +560,7 @@ static void parts_list_print(struct parts *parts, int indent) break; } - sys_message(" @ z=%d (%d)\n", parts->global.z, parts->local.z); + sys_message(" @ controller=%d z=%d (%d)\n", parts->controller_no, parts->global.z, parts->local.z); struct parts *child; PARTS_FOREACH_CHILD(child, parts) { diff --git a/src/parts/parts.c b/src/parts/parts.c index e5e5902..f121378 100644 --- a/src/parts/parts.c +++ b/src/parts/parts.c @@ -35,6 +35,12 @@ static struct parts_list dirty_list = TAILQ_HEAD_INITIALIZER(dirty_list); static struct hash_table *parts_table = NULL; static Point root_pos = { 0, 0 }; +struct parts_controller_stack ctrl_stack; +bool parts_multi_controller; + +static void ctrl_stack_init(void); +static void ctrl_stack_fini(void); + #define PARTS_PARAMS_INITIALIZER (struct parts_params) { \ .z = 1, \ .pos = { 0, 0 }, \ @@ -87,17 +93,38 @@ static void dirty_list_remove(struct parts *parts) TAILQ_REMOVE(&dirty_list, parts, dirty_list_entry); } +static int parts_get_sprite_z(struct parts *parts) +{ + if (!parts_multi_controller) + return parts->global.z; + // The system overlay controller sorts above any in-stack controller. + return parts->controller_no; +} + +static int parts_get_sprite_z2(struct parts *parts) +{ + if (!parts_multi_controller) + return 0; + return parts->global.z; +} + static void parts_list_insert(struct parts *parts) { + int z = parts_get_sprite_z(parts); + int z2 = parts_get_sprite_z2(parts); + parts->sp.z = z; + parts->sp.z2 = z2; struct parts *p; PARTS_LIST_FOREACH(p) { - if (p->global.z > parts->global.z) { + int pz = parts_get_sprite_z(p); + int pz2 = parts_get_sprite_z2(p); + if (pz > z || (pz == z && pz2 > z2)) { TAILQ_INSERT_BEFORE(p, parts, parts_list_entry); - parts_engine_dirty(); - return; + goto done; } } TAILQ_INSERT_TAIL(&parts_list, parts, parts_list_entry); +done: parts_engine_dirty(); scene_register_sprite(&parts->sp); } @@ -113,7 +140,6 @@ void parts_list_resort(struct parts *parts) // TODO: this could be optimized parts_list_remove(parts); parts_list_insert(parts); - scene_set_sprite_z(&parts->sp, parts->global.z); } struct parts *parts_try_get(int parts_no) @@ -132,6 +158,7 @@ struct parts *parts_get(int parts_no) struct parts *parts = parts_alloc(); parts->no = parts_no; + parts->controller_no = ctrl_stack.active; slot->value = parts; parts_list_insert(parts); return parts; @@ -968,6 +995,14 @@ void parts_release_all(void) static bool parts_engine_initialized = false; +void PE_enable_multi_controller(void) +{ + if (parts_multi_controller) + return; + assert(!parts_engine_initialized); + parts_multi_controller = true; +} + bool PE_Init(void) { if (parts_engine_initialized) @@ -977,6 +1012,7 @@ bool PE_Init(void) parts_table = ht_create(1024); parts_render_init(); parts_debug_init(); + ctrl_stack_init(); parts_engine_initialized = true; return true; } @@ -984,6 +1020,7 @@ bool PE_Init(void) void PE_Reset(void) { PE_ReleaseAllParts(); + ctrl_stack_fini(); sact_ModuleFini(); } @@ -1021,7 +1058,8 @@ static void parts_update_component(struct parts *parts) if (parts->parent) { parts_combine_params(&parts->parent->global, &parts->local, &parts->global); } - if (parts->global.z != parts->sp.z) { + if (parts_get_sprite_z(parts) != parts->sp.z + || parts_get_sprite_z2(parts) != parts->sp.z2) { parts_list_resort(parts); } @@ -1589,6 +1627,25 @@ void PE_ReleaseAllPartsWithoutSystem(void) parts_release_all(); } +void PE_ReleaseAllWithoutSystem(struct page **erase_number_list) +{ + // Release all parts not belonging to the system overlay controller + struct parts *parts = TAILQ_FIRST(&parts_list); + while (parts) { + struct parts *next = TAILQ_NEXT(parts, parts_list_entry); + if (parts->controller_no != PARTS_CONTROLLER_SYSTEM_OVERLAY) { + *erase_number_list = array_pushback(*erase_number_list, + (union vm_value){.i = parts->no}, AIN_ARRAY_INT, -1); + parts_release(parts->no); + } + parts = next; + } + + // Drop all normal controllers and add a fresh default one. + ctrl_stack.nr_controllers = 0; + PE_AddController(-1); +} + void PE_SetPos(int parts_no, int x, int y) { parts_set_pos(parts_get(parts_no), (Point){ x, y }); @@ -1880,6 +1937,94 @@ void PE_SetSpeedupRateByMessageSkip(int parts_no, int rate) UNIMPLEMENTED("(%d, %d)"); } +static void ctrl_stack_init(void) +{ + memset(&ctrl_stack, 0, sizeof(ctrl_stack)); + // Add initial default controller + PE_AddController(-1); +} + +static void ctrl_stack_fini(void) +{ + memset(&ctrl_stack, 0, sizeof(ctrl_stack)); +} + +// Adds a new controller to the stack and makes it active. The `index` +// parameter specifies the position in the stack at which to insert the new +// controller; -1 means "insert directly after the currently active +// controller". In practice the game only ever passes -1, and the active +// controller is always the top of the stack at that point, so this +// degenerates to a simple push. +int PE_AddController(int index) +{ + if (index != -1) + VM_ERROR("index != -1 not supported (got %d)", index); + if (ctrl_stack.nr_controllers > 0 && + ctrl_stack.active != ctrl_stack.nr_controllers - 1) + VM_ERROR("active controller is not at the top of the stack"); + if (ctrl_stack.nr_controllers >= PARTS_CONTROLLER_STACK_MAX) + VM_ERROR("controller stack overflow"); + + int no = ctrl_stack.nr_controllers++; + ctrl_stack.active = no; + return no; +} + +// Removes the controller at position `index` from the stack, releases all +// parts belonging to it, and returns their parts numbers in +// `erase_number_list`. `index == -1` means "remove the currently active +// controller". In practice the game only ever passes -1, and the active +// controller is always the top of the stack at that point, so this +// degenerates to a simple pop. +void PE_RemoveController(struct page **erase_number_list, int index) +{ + if (index != -1) + VM_ERROR("index != -1 not supported (got %d)", index); + if (ctrl_stack.nr_controllers == 0 || + ctrl_stack.active != ctrl_stack.nr_controllers - 1) + VM_ERROR("active controller is not at the top of the stack"); + + int ctrl_no = ctrl_stack.active; + + // Collect and release parts belonging to this controller + struct parts *p = TAILQ_FIRST(&parts_list); + while (p) { + struct parts *next = TAILQ_NEXT(p, parts_list_entry); + if (p->controller_no == ctrl_no) { + *erase_number_list = array_pushback(*erase_number_list, + (union vm_value){.i = p->no}, AIN_ARRAY_INT, -1); + parts_release(p->no); + } + p = next; + } + + ctrl_stack.nr_controllers--; + if (ctrl_stack.nr_controllers == 0) { + PE_AddController(-1); + } else { + ctrl_stack.active = ctrl_stack.nr_controllers - 1; + } +} + +void PE_set_active_controller(int controller_no) +{ + if (controller_no == PARTS_CONTROLLER_SYSTEM_OVERLAY || + (controller_no >= 0 && controller_no < ctrl_stack.nr_controllers)) + ctrl_stack.active = controller_no; + else + VM_ERROR("Invalid controller number: %d", controller_no); +} + +int PE_get_active_controller(void) +{ + return ctrl_stack.active; +} + +int PE_get_system_controller(void) +{ + return PARTS_CONTROLLER_SYSTEM_OVERLAY; +} + bool PE_init_parts_movie(int parts_no, int width, int height, int bg_r, int bg_g, int bg_b, int state) { if (!parts_state_valid(--state)) diff --git a/src/parts/parts_internal.h b/src/parts/parts_internal.h index 3054e1f..c94b488 100644 --- a/src/parts/parts_internal.h +++ b/src/parts/parts_internal.h @@ -374,6 +374,7 @@ struct parts { bool message_window; int alpha_clipper_parts_no; struct parts_motion_list motion; + int controller_no; }; #define PARTS_LIST_FOREACH(iter) TAILQ_FOREACH(iter, &parts_list, parts_list_entry) @@ -381,6 +382,19 @@ struct parts { // parts.c extern struct parts_list parts_list; + +// Controllers are identified by their position in the stack (0 = bottom). The +// system overlay controller lives outside the stack. +#define PARTS_CONTROLLER_STACK_MAX 10000 +#define PARTS_CONTROLLER_SYSTEM_OVERLAY PARTS_CONTROLLER_STACK_MAX + +struct parts_controller_stack { + int nr_controllers; + int active; // stack index or PARTS_CONTROLLER_SYSTEM_OVERLAY +}; +extern struct parts_controller_stack ctrl_stack; +extern bool parts_multi_controller; + struct parts *parts_try_get(int parts_no); struct parts *parts_get(int parts_no); struct parts_cg *parts_get_cg(struct parts *parts, int state); diff --git a/src/parts/save.c b/src/parts/save.c index 172a262..87af1bf 100644 --- a/src/parts/save.c +++ b/src/parts/save.c @@ -530,6 +530,10 @@ static void save_parts(struct iarray_writer *w, struct parts *parts) iarray_write(w, parts->draw_filter); iarray_write(w, parts->message_window); iarray_write(w, parts->alpha_clipper_parts_no); + // TODO: once the Rance 9 save format stabilizes, bump save version + // and save unconditionally + if (parts_multi_controller) + iarray_write(w, parts->controller_no); unsigned motion_count_pos = iarray_writer_pos(w); iarray_write(w, 0); // size of motion list @@ -569,6 +573,10 @@ static void load_parts(struct iarray_reader *r, int version) parts->message_window = iarray_read(r); if (version > 2) parts->alpha_clipper_parts_no = iarray_read(r); + // TODO: once the Rance 9 save format stabilizes, bump save version + // and load based on version check + if (parts_multi_controller) + parts->controller_no = iarray_read(r); int motion_count = iarray_read(r); for (int i = 0; i < motion_count; i++) { @@ -620,6 +628,13 @@ static bool parts_engine_save(struct page **buffer, bool save_hidden) if (CURRENT_SAVE_VERSION > 1) save_numeral_fonts(&w); + // TODO: once the Rance 9 save format stabilizes, bump save version + // and save unconditionally + if (parts_multi_controller) { + iarray_write(&w, ctrl_stack.active); + iarray_write(&w, ctrl_stack.nr_controllers); + } + unsigned count_pos = iarray_writer_pos(&w); iarray_write(&w, 0); // size of parts list @@ -677,6 +692,13 @@ bool PE_Load(struct page **buffer) if (version > 1) load_numeral_fonts(&r); + // TODO: once the Rance 9 save format stabilizes, bump save version + // and load based on version check + if (parts_multi_controller) { + ctrl_stack.active = iarray_read(&r); + ctrl_stack.nr_controllers = iarray_read(&r); + } + int nr_parts = iarray_read(&r); if (nr_parts < 0) { WARNING("invalid parts count");