From f80b7f42cd24da3a25cd4d4bf08451a27b9d1411 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 13:11:42 -0700 Subject: [PATCH 01/10] Allow distro hubs to select which city to send to --- C3X.h | 26 ++- injected_code.c | 431 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 379 insertions(+), 78 deletions(-) diff --git a/C3X.h b/C3X.h index c8965638..15027abb 100644 --- a/C3X.h +++ b/C3X.h @@ -208,10 +208,22 @@ enum distribution_hub_yield_division_mode { DHYDM_SCALE_BY_CITY_COUNT }; -enum ai_distribution_hub_build_strategy { - ADHBS_AUTO = 0, - ADHBS_BY_CITY_COUNT -}; +enum distribution_hub_city_selection_mode { + DHCSM_ALL_CITIES = 0, + DHCSM_SPECIFIC_CITIES +}; + +enum right_click_menu_item_id { + NAMED_TILE_MENU_ID = 0x90, + DISTRIBUTION_HUB_MENU_ALL_ID = 0x53000000, + DISTRIBUTION_HUB_MENU_SPECIFIC_ID = 0x53000001, + DISTRIBUTION_HUB_MENU_CITY_ID_BASE = 0x53000010 +}; + +enum ai_distribution_hub_build_strategy { + ADHBS_AUTO = 0, + ADHBS_BY_CITY_COUNT +}; enum ai_auto_build_great_wall_strategy { AAGWS_ALL_BORDERS = 0, @@ -1340,6 +1352,8 @@ struct distribution_hub_record { int tile_x; int tile_y; int civ_id; + int city_selection_mode; + struct table selected_city_ids; int food_yield; int shield_yield; int raw_food_yield; @@ -1597,6 +1611,10 @@ struct injected_state { bool named_tile_menu_active; int named_tile_menu_tile_x; int named_tile_menu_tile_y; + bool distribution_hub_menu_active; + int distribution_hub_menu_tile_x; + int distribution_hub_menu_tile_y; + bool distribution_hub_menu_reopen_requested; // List of temporary ints. Initializes to NULL/0/0, used with functions "memoize" and "clear_memo" int * memo; diff --git a/injected_code.c b/injected_code.c index 4bf1973a..7b69c642 100644 --- a/injected_code.c +++ b/injected_code.c @@ -92,10 +92,8 @@ struct injected_state * is = ADDR_INJECTED_STATE; // used to limit computational complexity #define AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES 10 -enum { NAMED_TILE_MENU_ID = 0x90 }; - -char const * const hotseat_replay_save_path = "Saves\\Auto\\ai-move-replay-before-interturn.SAV"; -char const * const hotseat_resume_save_path = "Saves\\Auto\\ai-move-replay-resume.SAV"; +char const * const hotseat_replay_save_path = "Saves\\Auto\\ai-move-replay-before-interturn.SAV"; +char const * const hotseat_resume_save_path = "Saves\\Auto\\ai-move-replay-resume.SAV"; // Need to define memmove for use by TCC when generated code for functions that return a struct void * @@ -6861,6 +6859,79 @@ distribution_hub_accessible_to_city (struct distribution_hub_record * rec, City return Trade_Net_have_trade_connection (is->trade_net, __, anchor_city, city, rec->civ_id); } +bool +distribution_hub_city_is_selected (struct distribution_hub_record * rec, City * city) +{ + if ((rec == NULL) || (city == NULL)) + return false; + + return itable_look_up_or (&rec->selected_city_ids, city->Body.ID, 0) != 0; +} + +bool +distribution_hub_distributes_to_city (struct distribution_hub_record * rec, City * city) +{ + if (! distribution_hub_accessible_to_city (rec, city)) + return false; + + if ((is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) && + (rec->city_selection_mode == DHCSM_SPECIFIC_CITIES)) + return distribution_hub_city_is_selected (rec, city); + + return true; +} + +void +clear_distribution_hub_city_selection (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + table_deinit (&rec->selected_city_ids); +} + +void +select_all_accessible_distribution_hub_cities (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + clear_distribution_hub_city_selection (rec); + FOR_CITIES_OF (coi, rec->civ_id) { + City * city = coi.city; + if ((city != NULL) && distribution_hub_accessible_to_city (rec, city)) + itable_insert (&rec->selected_city_ids, city->Body.ID, 1); + } +} + +int +count_distribution_hub_target_cities (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return 0; + + int count = 0; + FOR_CITIES_OF (coi, rec->civ_id) { + City * city = coi.city; + if ((city != NULL) && distribution_hub_distributes_to_city (rec, city)) + count++; + } + return count; +} + +void +recompute_distribution_hub_cities_for_civ (int civ_id) +{ + if ((civ_id < 0) || (civ_id >= 32) || (p_cities->Cities == NULL)) + return; + + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city->Body.CivID == civ_id)) + recompute_city_yields_with_districts (city); + } +} + void get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) { @@ -6876,7 +6947,7 @@ get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shi FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; - if (distribution_hub_accessible_to_city (rec, city)) { + if (distribution_hub_distributes_to_city (rec, city)) { food += rec->food_yield; shields += rec->shield_yield; } @@ -6957,6 +7028,7 @@ clear_distribution_hub_tables (void) { FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + clear_distribution_hub_city_selection (rec); free (rec); } table_deinit (&is->distribution_hub_records); @@ -7077,18 +7149,14 @@ recompute_distribution_hub_yields (struct distribution_hub_record * rec) if (shield_div <= 0) shield_div = 1; - int connected_city_count = 0; - if (anchor_city != NULL) { - FOR_CITIES_OF (coi, rec->civ_id) { - City * other_city = coi.city; - if ((other_city != NULL) && distribution_hub_accessible_to_city (rec, other_city)) - connected_city_count++; - } - } - if (connected_city_count <= 0) - connected_city_count = 1; + int connected_city_count = count_distribution_hub_target_cities (rec); if (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) { + if (connected_city_count <= 0) { + rec->food_yield = 0; + rec->shield_yield = 0; + return; + } int city_root = 1; while ((city_root + 1) * (city_root + 1) <= connected_city_count) city_root++; @@ -7114,18 +7182,12 @@ remove_distribution_hub_record (Tile * tile) int affected_civ_id = rec->civ_id; release_distribution_hub_coverage (rec); itable_remove (&is->distribution_hub_records, (int)tile); + clear_distribution_hub_city_selection (rec); free (rec); is->distribution_hub_totals_dirty = true; recompute_distribution_hub_totals (); - // Recalculate yields for all cities of this civ - if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } + recompute_distribution_hub_cities_for_civ (affected_civ_id); } void @@ -7166,9 +7228,14 @@ recompute_distribution_hub_totals () int old_civ_id = rec->civ_id; rec->civ_id = current_tile->vtable->m38_Get_Territory_OwnerID (current_tile); - if (old_civ_id != rec->civ_id) + if ((old_civ_id != rec->civ_id) && (old_civ_id >= 0) && (old_civ_id < 32)) civs_needing_recalc[old_civ_id] = 1; - civs_needing_recalc[rec->civ_id] = 1; + if ((rec->civ_id >= 0) && (rec->civ_id < 32)) + civs_needing_recalc[rec->civ_id] = 1; + if (old_civ_id != rec->civ_id) { + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + } City * anchor = get_connected_city_for_distribution_hub (rec); @@ -7249,13 +7316,8 @@ recompute_distribution_hub_totals () // Recalculate yields for cities of civs whose distribution hub ownership changed for (int civ_id = 0; civ_id < 32; civ_id++) { - if (civs_needing_recalc[civ_id] && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city->Body.CivID == civ_id)) - recompute_city_yields_with_districts (city); - } - } + if (civs_needing_recalc[civ_id]) + recompute_distribution_hub_cities_for_civ (civ_id); } is->distribution_hub_totals_dirty = false; @@ -7280,17 +7342,16 @@ on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) release_distribution_hub_coverage (rec); rec->civ_id = tile_owner; + if (old_civ_id != tile_owner) { + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + } is->distribution_hub_totals_dirty = true; recompute_distribution_hub_totals (); - if (old_civ_id != tile_owner) { - // Recompute for old civ - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == old_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } + recompute_distribution_hub_cities_for_civ (old_civ_id); + recompute_distribution_hub_cities_for_civ (tile_owner); + return; } rec = malloc (sizeof *rec); @@ -7300,6 +7361,8 @@ on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) rec->tile_x = tile_x; rec->tile_y = tile_y; rec->civ_id = tile_owner; + rec->city_selection_mode = DHCSM_ALL_CITIES; + memset (&rec->selected_city_ids, 0, sizeof rec->selected_city_ids); rec->food_yield = 0; rec->shield_yield = 0; rec->raw_food_yield = 0; @@ -7310,15 +7373,8 @@ on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) is->distribution_hub_totals_dirty = true; recompute_distribution_hub_totals (); - // Recalculate yields for all cities of this civ int affected_civ_id = rec->civ_id; - if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } + recompute_distribution_hub_cities_for_civ (affected_civ_id); } void @@ -24089,6 +24145,142 @@ patch_Context_Menu_add_item_and_set_color (Context_Menu * this, int edx, int ite return tr; } +struct distribution_hub_record * +get_active_distribution_hub_menu_record (void) +{ + if (! is->distribution_hub_menu_active) + return NULL; + + if (! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return NULL; + + Tile * tile = tile_at (is->distribution_hub_menu_tile_x, is->distribution_hub_menu_tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || + (inst->district_id != DISTRIBUTION_HUB_DISTRICT_ID) || + ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID)) + return NULL; + + if (is->distribution_hub_totals_dirty && + ! is->distribution_hub_refresh_in_progress) + recompute_distribution_hub_totals (); + + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec == NULL) { + on_distribution_hub_completed (tile, is->distribution_hub_menu_tile_x, is->distribution_hub_menu_tile_y); + rec = get_distribution_hub_record (tile); + } + if ((rec == NULL) || (rec->civ_id != p_main_screen_form->Player_CivID)) + return NULL; + + return rec; +} + +bool +distribution_hub_menu_can_open_on_tile (Tile * tile, int tile_x, int tile_y) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT) || + (tile == NULL) || + (tile == p_null_tile)) + return false; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || + (inst->district_id != DISTRIBUTION_HUB_DISTRICT_ID) || + ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID)) + return false; + + int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + return owner == p_main_screen_form->Player_CivID; +} + +void +add_distribution_hub_menu_items (Context_Menu * menu, struct distribution_hub_record * rec) +{ + if ((menu == NULL) || (rec == NULL)) + return; + + if (menu->Item_Count > 0) + Context_Menu_add_separator (menu, __, 0); + + bool specific = rec->city_selection_mode == DHCSM_SPECIFIC_CITIES; + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID, "Distribute to All Cities", ! specific, (Sprite *)0x0); + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID, "Distribute to Specific Cities", specific, (Sprite *)0x0); + Context_Menu_add_separator (menu, __, 0); + + FOR_CITIES_OF (coi, rec->civ_id) { + City * city = coi.city; + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + continue; + + int food_yield = 0; + int shield_yield = 0; + if (distribution_hub_distributes_to_city (rec, city)) { + food_yield = rec->food_yield; + shield_yield = rec->shield_yield; + } + + char menu_text[160]; + if ((food_yield > 0) || (shield_yield > 0)) + snprintf (menu_text, sizeof menu_text, "%s %d food %d shields", city->Body.CityName, food_yield, shield_yield); + else + snprintf (menu_text, sizeof menu_text, "%s", city->Body.CityName); + menu_text[sizeof menu_text - 1] = '\0'; + + bool selected = specific && distribution_hub_city_is_selected (rec, city); + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_CITY_ID_BASE + city->Body.ID, menu_text, selected, (Sprite *)0x0); + } +} + +bool +handle_distribution_hub_menu_selection (int item_id) +{ + struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); + if (rec == NULL) + return false; + + int affected_civ_id = rec->civ_id; + + if (item_id == DISTRIBUTION_HUB_MENU_ALL_ID) { + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + } else if (item_id == DISTRIBUTION_HUB_MENU_SPECIFIC_ID) { + if (rec->city_selection_mode != DHCSM_SPECIFIC_CITIES) + select_all_accessible_distribution_hub_cities (rec); + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + } else if (item_id >= DISTRIBUTION_HUB_MENU_CITY_ID_BASE) { + int city_id = item_id - DISTRIBUTION_HUB_MENU_CITY_ID_BASE; + City * city = get_city_ptr (city_id); + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + return false; + + if (rec->city_selection_mode != DHCSM_SPECIFIC_CITIES) { + select_all_accessible_distribution_hub_cities (rec); + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + } + + if (distribution_hub_city_is_selected (rec, city)) + itable_remove (&rec->selected_city_ids, city->Body.ID); + else + itable_insert (&rec->selected_city_ids, city->Body.ID, 1); + } else + return false; + + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (affected_civ_id); + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + is->distribution_hub_menu_reopen_requested = true; + return true; +} + int __fastcall patch_Context_Menu_open (Context_Menu * this, int edx, int x, int y, int param_3) { @@ -24113,6 +24305,10 @@ patch_Context_Menu_open (Context_Menu * this, int edx, int x, int y, int param_3 } } + struct distribution_hub_record * hub_rec = get_active_distribution_hub_menu_record (); + if (hub_rec != NULL) + add_distribution_hub_menu_items (this, hub_rec); + if (is->current_config.group_units_on_right_click_menu && (ret_addr == ADDR_OPEN_UNIT_MENU_RETURN) && (is->unit_menu_duplicates != NULL)) { @@ -25874,16 +26070,26 @@ handle_named_tile_menu_selection (void) void __fastcall patch_Main_Screen_Form_handle_right_click_on_tile (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y) { - if (is->current_config.enable_named_tiles) { - Tile * tile = tile_at (tile_x, tile_y); - if (tile_can_be_named (tile, tile_x, tile_y) && ! Tile_has_city (tile)) { - is->named_tile_menu_active = true; - is->named_tile_menu_tile_x = tile_x; - is->named_tile_menu_tile_y = tile_y; + Tile * tile = tile_at (tile_x, tile_y); + bool named_tile_active = is->current_config.enable_named_tiles && tile_can_be_named (tile, tile_x, tile_y) && ! Tile_has_city (tile); + bool hub_menu_active = distribution_hub_menu_can_open_on_tile (tile, tile_x, tile_y); + + if (named_tile_active || hub_menu_active) { + is->named_tile_menu_active = named_tile_active; + is->named_tile_menu_tile_x = tile_x; + is->named_tile_menu_tile_y = tile_y; + is->distribution_hub_menu_active = hub_menu_active; + is->distribution_hub_menu_tile_x = tile_x; + is->distribution_hub_menu_tile_y = tile_y; + do { + is->distribution_hub_menu_reopen_requested = false; Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); - is->named_tile_menu_active = false; - return; - } + } while (is->distribution_hub_menu_reopen_requested && + distribution_hub_menu_can_open_on_tile (tile_at (tile_x, tile_y), tile_x, tile_y)); + is->named_tile_menu_active = false; + is->distribution_hub_menu_active = false; + is->distribution_hub_menu_reopen_requested = false; + return; } Main_Screen_Form_handle_right_click_on_tile (this, __, tile_x, tile_y, mouse_x, mouse_y); @@ -25893,18 +26099,30 @@ void __fastcall patch_Main_Screen_Form_open_right_click_menu (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y) { bool set_active = false; - if (!is->named_tile_menu_active && is->current_config.enable_named_tiles) { + if (! is->named_tile_menu_active && ! is->distribution_hub_menu_active) { Tile * tile = tile_at (tile_x, tile_y); - if (tile_can_be_named (tile, tile_x, tile_y)) { - is->named_tile_menu_active = true; + bool named_tile_active = is->current_config.enable_named_tiles && tile_can_be_named (tile, tile_x, tile_y); + bool hub_menu_active = distribution_hub_menu_can_open_on_tile (tile, tile_x, tile_y); + if (named_tile_active || hub_menu_active) { + is->named_tile_menu_active = named_tile_active; is->named_tile_menu_tile_x = tile_x; is->named_tile_menu_tile_y = tile_y; + is->distribution_hub_menu_active = hub_menu_active; + is->distribution_hub_menu_tile_x = tile_x; + is->distribution_hub_menu_tile_y = tile_y; set_active = true; } } - Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); - if (set_active) + do { + is->distribution_hub_menu_reopen_requested = false; + Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); + } while (is->distribution_hub_menu_reopen_requested && + distribution_hub_menu_can_open_on_tile (tile_at (tile_x, tile_y), tile_x, tile_y)); + if (set_active) { is->named_tile_menu_active = false; + is->distribution_hub_menu_active = false; + is->distribution_hub_menu_reopen_requested = false; + } } void @@ -30560,9 +30778,12 @@ patch_Main_Screen_Form_open_quick_build_chooser (Main_Screen_Form * this, int ed { recompute_resources_if_necessary (); bool restore_named_tile_menu = is->named_tile_menu_active; + bool restore_distribution_hub_menu = is->distribution_hub_menu_active; is->named_tile_menu_active = false; + is->distribution_hub_menu_active = false; Main_Screen_Form_open_quick_build_chooser (this, __, city, mouse_x, mouse_y); is->named_tile_menu_active = restore_named_tile_menu; + is->distribution_hub_menu_active = restore_distribution_hub_menu; } int __fastcall @@ -30572,6 +30793,11 @@ patch_Context_Menu_get_selected_item_on_unit_rcm (Context_Menu * this) // click unit items which have been disabled by the mod so they can interrupt the queued actions of units that have no moves left. int index = this->Selected_Item; if (index >= 0) { + if (is->distribution_hub_menu_active) { + Context_Menu_Item * item = &this->Items[index]; + if (handle_distribution_hub_menu_selection (item->Menu_Item_ID)) + return -1; + } if (is->current_config.enable_named_tiles && is->named_tile_menu_active) { Context_Menu_Item * item = &this->Items[index]; if (item->Menu_Item_ID == NAMED_TILE_MENU_ID) { @@ -30958,8 +31184,14 @@ patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR fi is->current_config.enable_distribution_hub_districts && (is->distribution_hub_records.len > 0)) { serialize_aligned_text ("distribution_hub_records", &mod_data); - int entry_capacity = is->distribution_hub_records.len; - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); + int int_count = 1; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if (rec == NULL) + continue; + int_count += 5 + (int)rec->selected_city_ids.len; + } + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * int_count); int * out = chunk + 1; int written = 0; FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { @@ -30969,15 +31201,15 @@ patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR fi out[0] = rec->tile_x; out[1] = rec->tile_y; out[2] = rec->civ_id; - out += 3; + out[3] = rec->city_selection_mode; + out[4] = (int)rec->selected_city_ids.len; + out += 5; + FOR_TABLE_ENTRIES (selected_tei, &rec->selected_city_ids) { + *out++ = selected_tei.key; + } written++; } - chunk[0] = written; - int unused_entries = entry_capacity - written; - if (unused_entries > 0) { - int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); - mod_data.length -= trimmed_bytes; - } + chunk[0] = -written; } if (is->current_config.enable_districts && @@ -31559,7 +31791,55 @@ patch_move_game_data (byte * buffer, bool save_else_load) int entry_count = *ints++; cursor = (byte *)ints; remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { + if (entry_count < 0) { + entry_count = -entry_count; + clear_distribution_hub_tables (); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 5 * (int)sizeof(int)) { + success = false; + break; + } + int x = *ints++; + int y = *ints++; + int civ_id = *ints++; + int selection_mode = *ints++; + int selected_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 5 * (int)sizeof(int); + if ((selected_count < 0) || + (remaining_bytes < selected_count * (int)sizeof(int))) { + success = false; + break; + } + + Tile * tile = tile_at (x, y); + if ((tile != NULL) && (tile != p_null_tile)) { + on_distribution_hub_completed (tile, x, y); + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec != NULL) { + rec->civ_id = civ_id; + if (selection_mode == DHCSM_SPECIFIC_CITIES) + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + else + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + for (int selected_index = 0; selected_index < selected_count; selected_index++) { + int city_id = ints[selected_index]; + itable_insert (&rec->selected_city_ids, city_id, 1); + } + } + } + ints += selected_count; + cursor = (byte *)ints; + remaining_bytes -= selected_count * (int)sizeof(int); + } + if (success) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + } + } + else if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { clear_distribution_hub_tables (); success = true; for (int n = 0; n < entry_count; n++) { @@ -31580,6 +31860,10 @@ patch_move_game_data (byte * buffer, bool save_else_load) if (rec != NULL) rec->civ_id = civ_id; } + if (success) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + } } } if (! success) { @@ -32866,8 +33150,7 @@ draw_distribution_hub_yields (City_Form * city_form, Tile * tile, int tile_x, in if (rec == NULL) return; - City * anchor_city = get_connected_city_for_distribution_hub (rec); - if (! distribution_hub_accessible_to_city (rec, city_form->CurrentCity)) + if (! distribution_hub_distributes_to_city (rec, city_form->CurrentCity)) return; int food_yield = rec->food_yield; From 8e53c66280f7c6db117c8f62cfc52208687c4ed9 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 13:22:02 -0700 Subject: [PATCH 02/10] Ensure AI selects cities --- injected_code.c | 241 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 180 insertions(+), 61 deletions(-) diff --git a/injected_code.c b/injected_code.c index 7b69c642..06473ee7 100644 --- a/injected_code.c +++ b/injected_code.c @@ -6904,37 +6904,156 @@ select_all_accessible_distribution_hub_cities (struct distribution_hub_record * } } -int -count_distribution_hub_target_cities (struct distribution_hub_record * rec) -{ - if (rec == NULL) - return 0; +int +count_distribution_hub_target_cities (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return 0; int count = 0; FOR_CITIES_OF (coi, rec->civ_id) { City * city = coi.city; if ((city != NULL) && distribution_hub_distributes_to_city (rec, city)) count++; - } - return count; -} - -void -recompute_distribution_hub_cities_for_civ (int civ_id) -{ - if ((civ_id < 0) || (civ_id >= 32) || (p_cities->Cities == NULL)) - return; - - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city->Body.CivID == civ_id)) - recompute_city_yields_with_districts (city); - } -} - -void -get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) -{ + } + return count; +} + +void +recompute_distribution_hub_cities_for_civ (int civ_id) +{ + if ((civ_id < 0) || (civ_id >= 32) || (p_cities->Cities == NULL)) + return; + + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city->Body.CivID == civ_id)) + recompute_city_yields_with_districts (city); + } +} + +int +ai_score_distribution_hub_target_city (City * city) +{ + if (city == NULL) + return 0; + + int score = 0; + + int net_food = city->Body.FoodIncome; + if (net_food <= 0) + score += 1000; + else if (net_food <= 2) + score += 500; + + int net_shields = city->Body.ProductionIncome + city->Body.ProductionLoss; + if (net_shields < 0) + net_shields = 0; + if (net_shields <= 3) + score += 700; + else if (net_shields <= 6) + score += 350; + + return score; +} + +bool +ai_update_distribution_hub_city_selection (struct distribution_hub_record * rec) +{ + if ((rec == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return false; + + int civ_id = rec->civ_id; + if ((civ_id < 0) || (civ_id >= 32)) + return false; + if ((1u << civ_id) & *p_human_player_bits) + return false; + + struct table selected = {0}; + int accessible_count = 0; + int selected_count = 0; + + FOR_CITIES_OF (coi, civ_id) { + City * city = coi.city; + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + continue; + + accessible_count++; + if (ai_score_distribution_hub_target_city (city) > 0) { + itable_insert (&selected, city->Body.ID, 1); + selected_count++; + } + } + + if ((accessible_count <= 1) || + (selected_count <= 0) || + (selected_count >= accessible_count)) { + table_deinit (&selected); + if ((rec->city_selection_mode == DHCSM_ALL_CITIES) && (rec->selected_city_ids.len == 0)) + return false; + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + return true; + } + + bool changed = rec->city_selection_mode != DHCSM_SPECIFIC_CITIES; + if (rec->selected_city_ids.len != selected.len) + changed = true; + else { + FOR_TABLE_ENTRIES (tei, &selected) { + if (! itable_look_up_or (&rec->selected_city_ids, tei.key, 0)) { + changed = true; + break; + } + } + } + + if (! changed) { + table_deinit (&selected); + return false; + } + + clear_distribution_hub_city_selection (rec); + rec->selected_city_ids = selected; + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + return true; +} + +void +ai_update_distribution_hub_city_selections_for_leader (Leader * leader) +{ + if ((leader == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return; + + int civ_id = leader->ID; + if ((civ_id < 0) || (civ_id >= 32)) + return; + if ((1u << civ_id) & *p_human_player_bits) + return; + + bool changed = false; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if ((rec != NULL) && (rec->civ_id == civ_id) && ai_update_distribution_hub_city_selection (rec)) + changed = true; + } + + if (changed) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (civ_id); + } +} + +void +get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) +{ int food = 0; int shields = 0; @@ -13934,10 +14053,10 @@ find_tile_for_neighborhood_district (City * city, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } if (! tile_suitable_for_district (tile, NEIGHBORHOOD_DISTRICT_ID, city, &has_resource)) @@ -14002,10 +14121,10 @@ find_tile_for_port_district (City * city, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } if (! tile_suitable_for_district (tile, PORT_DISTRICT_ID, city, &has_resource)) @@ -14594,10 +14713,10 @@ find_tile_for_district (City * city, int district_id, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } if (! tile_suitable_for_district (tile, district_id, city, &has_resource)) @@ -26415,11 +26534,11 @@ on_gain_city (Leader * leader, City * city, enum city_gain_reason reason) grant_nearby_wonders_to_city (city); } - if (is->current_config.enable_distribution_hub_districts) { - refresh_distribution_hubs_for_city (city); - } - } -} + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + refresh_distribution_hubs_for_city (city); + } + } +} void on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) @@ -26437,11 +26556,10 @@ on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) for (int district_id = 0; district_id < is->district_count; district_id++) clear_city_district_request (city, district_id); - if (is->current_config.enable_wonder_districts) - release_wonder_district_reservation (city); - } else if (is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; -} + if (is->current_config.enable_wonder_districts) + release_wonder_district_reservation (city); + } +} // Returns -1 if the location is unusable, 0-9 if it's usable but doesn't satisfy all criteria, and 10 if it couldn't be better int @@ -28393,10 +28511,11 @@ patch_Leader_do_production_phase (Leader * this) auto_dynamic_district_ids[auto_dynamic_district_count++] = CENTRAL_RAIL_HUB_DISTRICT_ID; } - if (is->current_config.enable_distribution_hub_districts) { - if (leader_can_build_district (this, DISTRIBUTION_HUB_DISTRICT_ID)) - ai_update_distribution_hub_goal_for_leader (this); - } + if (is->current_config.enable_distribution_hub_districts) { + if (leader_can_build_district (this, DISTRIBUTION_HUB_DISTRICT_ID)) + ai_update_distribution_hub_goal_for_leader (this); + ai_update_distribution_hub_city_selections_for_leader (this); + } FOR_CITIES_OF (coi, this->ID) { City * city = coi.city; @@ -35612,12 +35731,12 @@ draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instanc variant = culture; draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); return; - } - case DISTRIBUTION_HUB_DISTRICT_ID: - if (! is->current_config.enable_distribution_hub_districts) - return; - - draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + } + case DISTRIBUTION_HUB_DISTRICT_ID: + if (! is->current_config.enable_districts || ! is->current_config.enable_distribution_hub_districts) + return; + + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); return; case ENERGY_GRID_DISTRICT_ID: { @@ -36471,10 +36590,10 @@ recompute_district_and_distribution_hub_shields_for_city_view (City * city) total_district_shield_bonus += shield_bonus; } - // Distribution hub contribution is tracked separately for icon rendering. - int distribution_hub_shields = 0; - if (is->current_config.enable_distribution_hub_districts) - get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); + // Distribution hub contribution is tracked separately for icon rendering. + int distribution_hub_shields = 0; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) + get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); if (distribution_hub_shields < 0) distribution_hub_shields = 0; if (distribution_hub_shields > city_center_district_shields) From ccc0d240d0d80616197206be9415f0f03fa762aa Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 14:22:27 -0700 Subject: [PATCH 03/10] Add icons to select menu, make sure counts align with city view --- C3X.h | 33 +-- injected_code.c | 622 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 433 insertions(+), 222 deletions(-) diff --git a/C3X.h b/C3X.h index 15027abb..3fae44a7 100644 --- a/C3X.h +++ b/C3X.h @@ -208,22 +208,22 @@ enum distribution_hub_yield_division_mode { DHYDM_SCALE_BY_CITY_COUNT }; -enum distribution_hub_city_selection_mode { - DHCSM_ALL_CITIES = 0, - DHCSM_SPECIFIC_CITIES -}; - -enum right_click_menu_item_id { - NAMED_TILE_MENU_ID = 0x90, - DISTRIBUTION_HUB_MENU_ALL_ID = 0x53000000, - DISTRIBUTION_HUB_MENU_SPECIFIC_ID = 0x53000001, - DISTRIBUTION_HUB_MENU_CITY_ID_BASE = 0x53000010 -}; - -enum ai_distribution_hub_build_strategy { - ADHBS_AUTO = 0, - ADHBS_BY_CITY_COUNT -}; +enum distribution_hub_city_selection_mode { + DHCSM_ALL_CITIES = 0, + DHCSM_SPECIFIC_CITIES +}; + +enum right_click_menu_item_id { + NAMED_TILE_MENU_ID = 0x90, + DISTRIBUTION_HUB_MENU_ALL_ID = 0x53000000, + DISTRIBUTION_HUB_MENU_SPECIFIC_ID = 0x53000001, + DISTRIBUTION_HUB_MENU_CITY_ID_BASE = 0x53000010 +}; + +enum ai_distribution_hub_build_strategy { + ADHBS_AUTO = 0, + ADHBS_BY_CITY_COUNT +}; enum ai_auto_build_great_wall_strategy { AAGWS_ALL_BORDERS = 0, @@ -2259,6 +2259,7 @@ struct district_button_image_set { Sprite distribution_hub_eaten_food_icon; Sprite distribution_hub_shield_icon_small; Sprite distribution_hub_food_icon_small; + Sprite distribution_hub_menu_icon_sentinel; int non_district_shield_icons_remaining; int corruption_shield_icons_remaining; int district_shield_icons_remaining; diff --git a/injected_code.c b/injected_code.c index 06473ee7..fc313f7b 100644 --- a/injected_code.c +++ b/injected_code.c @@ -92,8 +92,8 @@ struct injected_state * is = ADDR_INJECTED_STATE; // used to limit computational complexity #define AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES 10 -char const * const hotseat_replay_save_path = "Saves\\Auto\\ai-move-replay-before-interturn.SAV"; -char const * const hotseat_resume_save_path = "Saves\\Auto\\ai-move-replay-resume.SAV"; +char const * const hotseat_replay_save_path = "Saves\\Auto\\ai-move-replay-before-interturn.SAV"; +char const * const hotseat_resume_save_path = "Saves\\Auto\\ai-move-replay-resume.SAV"; // Need to define memmove for use by TCC when generated code for functions that return a struct void * @@ -244,6 +244,7 @@ bool tile_coords_has_city_with_building_in_district_radius (int tile_x, int tile void __fastcall patch_Trade_Net_recompute_resources (Trade_Net * this, int edx, bool skip_popups); int get_visible_non_subsumed_tile_resource (Tile * tile, struct district_instance * inst, int civ_id); void recompute_distribution_hub_totals (); +void init_distribution_hub_icons (); void get_neighbor_coords (Map * map, int x, int y, int neighbor_index, int * out_x, int * out_y); void wrap_tile_coords (Map * map, int * x, int * y); int count_neighborhoods_in_city_radius (City * city); @@ -6904,156 +6905,156 @@ select_all_accessible_distribution_hub_cities (struct distribution_hub_record * } } -int -count_distribution_hub_target_cities (struct distribution_hub_record * rec) -{ - if (rec == NULL) - return 0; +int +count_distribution_hub_target_cities (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return 0; int count = 0; FOR_CITIES_OF (coi, rec->civ_id) { City * city = coi.city; if ((city != NULL) && distribution_hub_distributes_to_city (rec, city)) count++; - } - return count; -} - -void -recompute_distribution_hub_cities_for_civ (int civ_id) -{ - if ((civ_id < 0) || (civ_id >= 32) || (p_cities->Cities == NULL)) - return; - - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city->Body.CivID == civ_id)) - recompute_city_yields_with_districts (city); - } -} - -int -ai_score_distribution_hub_target_city (City * city) -{ - if (city == NULL) - return 0; - - int score = 0; - - int net_food = city->Body.FoodIncome; - if (net_food <= 0) - score += 1000; - else if (net_food <= 2) - score += 500; - - int net_shields = city->Body.ProductionIncome + city->Body.ProductionLoss; - if (net_shields < 0) - net_shields = 0; - if (net_shields <= 3) - score += 700; - else if (net_shields <= 6) - score += 350; - - return score; -} - -bool -ai_update_distribution_hub_city_selection (struct distribution_hub_record * rec) -{ - if ((rec == NULL) || - ! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) - return false; - - int civ_id = rec->civ_id; - if ((civ_id < 0) || (civ_id >= 32)) - return false; - if ((1u << civ_id) & *p_human_player_bits) - return false; - - struct table selected = {0}; - int accessible_count = 0; - int selected_count = 0; - - FOR_CITIES_OF (coi, civ_id) { - City * city = coi.city; - if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) - continue; - - accessible_count++; - if (ai_score_distribution_hub_target_city (city) > 0) { - itable_insert (&selected, city->Body.ID, 1); - selected_count++; - } - } - - if ((accessible_count <= 1) || - (selected_count <= 0) || - (selected_count >= accessible_count)) { - table_deinit (&selected); - if ((rec->city_selection_mode == DHCSM_ALL_CITIES) && (rec->selected_city_ids.len == 0)) - return false; - rec->city_selection_mode = DHCSM_ALL_CITIES; - clear_distribution_hub_city_selection (rec); - return true; - } - - bool changed = rec->city_selection_mode != DHCSM_SPECIFIC_CITIES; - if (rec->selected_city_ids.len != selected.len) - changed = true; - else { - FOR_TABLE_ENTRIES (tei, &selected) { - if (! itable_look_up_or (&rec->selected_city_ids, tei.key, 0)) { - changed = true; - break; - } - } - } - - if (! changed) { - table_deinit (&selected); - return false; - } - - clear_distribution_hub_city_selection (rec); - rec->selected_city_ids = selected; - rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; - return true; -} - -void -ai_update_distribution_hub_city_selections_for_leader (Leader * leader) -{ - if ((leader == NULL) || - ! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) - return; - - int civ_id = leader->ID; - if ((civ_id < 0) || (civ_id >= 32)) - return; - if ((1u << civ_id) & *p_human_player_bits) - return; - - bool changed = false; - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; - if ((rec != NULL) && (rec->civ_id == civ_id) && ai_update_distribution_hub_city_selection (rec)) - changed = true; - } - - if (changed) { - is->distribution_hub_totals_dirty = true; - recompute_distribution_hub_totals (); - recompute_distribution_hub_cities_for_civ (civ_id); - } -} - -void -get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) -{ + } + return count; +} + +void +recompute_distribution_hub_cities_for_civ (int civ_id) +{ + if ((civ_id < 0) || (civ_id >= 32) || (p_cities->Cities == NULL)) + return; + + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city->Body.CivID == civ_id)) + recompute_city_yields_with_districts (city); + } +} + +int +ai_score_distribution_hub_target_city (City * city) +{ + if (city == NULL) + return 0; + + int score = 0; + + int net_food = city->Body.FoodIncome; + if (net_food <= 0) + score += 1000; + else if (net_food <= 2) + score += 500; + + int net_shields = city->Body.ProductionIncome + city->Body.ProductionLoss; + if (net_shields < 0) + net_shields = 0; + if (net_shields <= 3) + score += 700; + else if (net_shields <= 6) + score += 350; + + return score; +} + +bool +ai_update_distribution_hub_city_selection (struct distribution_hub_record * rec) +{ + if ((rec == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return false; + + int civ_id = rec->civ_id; + if ((civ_id < 0) || (civ_id >= 32)) + return false; + if ((1u << civ_id) & *p_human_player_bits) + return false; + + struct table selected = {0}; + int accessible_count = 0; + int selected_count = 0; + + FOR_CITIES_OF (coi, civ_id) { + City * city = coi.city; + if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) + continue; + + accessible_count++; + if (ai_score_distribution_hub_target_city (city) > 0) { + itable_insert (&selected, city->Body.ID, 1); + selected_count++; + } + } + + if ((accessible_count <= 1) || + (selected_count <= 0) || + (selected_count >= accessible_count)) { + table_deinit (&selected); + if ((rec->city_selection_mode == DHCSM_ALL_CITIES) && (rec->selected_city_ids.len == 0)) + return false; + rec->city_selection_mode = DHCSM_ALL_CITIES; + clear_distribution_hub_city_selection (rec); + return true; + } + + bool changed = rec->city_selection_mode != DHCSM_SPECIFIC_CITIES; + if (rec->selected_city_ids.len != selected.len) + changed = true; + else { + FOR_TABLE_ENTRIES (tei, &selected) { + if (! itable_look_up_or (&rec->selected_city_ids, tei.key, 0)) { + changed = true; + break; + } + } + } + + if (! changed) { + table_deinit (&selected); + return false; + } + + clear_distribution_hub_city_selection (rec); + rec->selected_city_ids = selected; + rec->city_selection_mode = DHCSM_SPECIFIC_CITIES; + return true; +} + +void +ai_update_distribution_hub_city_selections_for_leader (Leader * leader) +{ + if ((leader == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return; + + int civ_id = leader->ID; + if ((civ_id < 0) || (civ_id >= 32)) + return; + if ((1u << civ_id) & *p_human_player_bits) + return; + + bool changed = false; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if ((rec != NULL) && (rec->civ_id == civ_id) && ai_update_distribution_hub_city_selection (rec)) + changed = true; + } + + if (changed) { + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (civ_id); + } +} + +void +get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) +{ int food = 0; int shields = 0; @@ -7289,6 +7290,11 @@ recompute_distribution_hub_yields (struct distribution_hub_record * rec) rec->food_yield = food_sum / food_div; rec->shield_yield = shield_sum / shield_div; } + + if ((food_sum > 0) && (rec->food_yield <= 0)) + rec->food_yield = 1; + if ((shield_sum > 0) && (rec->shield_yield <= 0)) + rec->shield_yield = 1; } void @@ -14053,10 +14059,10 @@ find_tile_for_neighborhood_district (City * city, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } if (! tile_suitable_for_district (tile, NEIGHBORHOOD_DISTRICT_ID, city, &has_resource)) @@ -14121,10 +14127,10 @@ find_tile_for_port_district (City * city, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } if (! tile_suitable_for_district (tile, PORT_DISTRICT_ID, city, &has_resource)) @@ -14713,10 +14719,10 @@ find_tile_for_district (City * city, int district_id, int * out_x, int * out_y) Tile * tile = tri.tile; bool has_resource = false; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } if (! tile_suitable_for_district (tile, district_id, city, &has_resource)) @@ -24264,15 +24270,21 @@ patch_Context_Menu_add_item_and_set_color (Context_Menu * this, int edx, int ite return tr; } +bool +distribution_hub_city_select_ui_enabled (void) +{ + return is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts && + (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT); +} + struct distribution_hub_record * get_active_distribution_hub_menu_record (void) { if (! is->distribution_hub_menu_active) return NULL; - if (! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + if (! distribution_hub_city_select_ui_enabled ()) return NULL; Tile * tile = tile_at (is->distribution_hub_menu_tile_x, is->distribution_hub_menu_tile_y); @@ -24303,9 +24315,7 @@ get_active_distribution_hub_menu_record (void) bool distribution_hub_menu_can_open_on_tile (Tile * tile, int tile_x, int tile_y) { - if (! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT) || + if (! distribution_hub_city_select_ui_enabled () || (tile == NULL) || (tile == p_null_tile)) return false; @@ -24330,8 +24340,12 @@ add_distribution_hub_menu_items (Context_Menu * menu, struct distribution_hub_re Context_Menu_add_separator (menu, __, 0); bool specific = rec->city_selection_mode == DHCSM_SPECIFIC_CITIES; - Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID, "Distribute to All Cities", ! specific, (Sprite *)0x0); - Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID, "Distribute to Specific Cities", specific, (Sprite *)0x0); + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID, "Distribute to All Cities", false, (Sprite *)0x0); + if (specific) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID); + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID, "Distribute to Specific Cities", false, (Sprite *)0x0); + if (! specific) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID); Context_Menu_add_separator (menu, __, 0); FOR_CITIES_OF (coi, rec->civ_id) { @@ -24339,22 +24353,17 @@ add_distribution_hub_menu_items (Context_Menu * menu, struct distribution_hub_re if ((city == NULL) || ! distribution_hub_accessible_to_city (rec, city)) continue; - int food_yield = 0; - int shield_yield = 0; - if (distribution_hub_distributes_to_city (rec, city)) { - food_yield = rec->food_yield; - shield_yield = rec->shield_yield; - } - char menu_text[160]; - if ((food_yield > 0) || (shield_yield > 0)) - snprintf (menu_text, sizeof menu_text, "%s %d food %d shields", city->Body.CityName, food_yield, shield_yield); - else - snprintf (menu_text, sizeof menu_text, "%s", city->Body.CityName); + snprintf (menu_text, sizeof menu_text, "%s", city->Body.CityName); menu_text[sizeof menu_text - 1] = '\0'; - bool selected = specific && distribution_hub_city_is_selected (rec, city); - Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_CITY_ID_BASE + city->Body.ID, menu_text, selected, (Sprite *)0x0); + Sprite * icon_sentinel = NULL; + if (distribution_hub_distributes_to_city (rec, city) && + ((rec->food_yield > 0) || (rec->shield_yield > 0))) + icon_sentinel = &is->distribution_hub_menu_icon_sentinel; + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_CITY_ID_BASE + city->Body.ID, menu_text, false, icon_sentinel); + if (specific && ! distribution_hub_city_is_selected (rec, city)) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_CITY_ID_BASE + city->Body.ID); } } @@ -24884,9 +24893,93 @@ patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool return tr; } +void +draw_distribution_hub_menu_icons (PCX_Image * canvas, int image_x, int row_center_y) +{ + if ((canvas == NULL) || + (canvas->JGL.Image == NULL) || + ! is->distribution_hub_menu_active) + return; + + struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); + if (rec == NULL) + return; + + int food_yield = rec->food_yield; + int shield_yield = rec->shield_yield; + int total_yield = food_yield + shield_yield; + if (total_yield <= 0) + return; + + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->distribution_hub_icons_img_state != IS_OK) + return; + + Sprite * food_sprite = &is->distribution_hub_food_icon_small; + Sprite * shield_sprite = &is->distribution_hub_shield_icon_small; + + if (food_sprite->Width3 == 0) food_sprite = &is->distribution_hub_food_icon; + if (shield_sprite->Width3 == 0) shield_sprite = &is->distribution_hub_shield_icon; + + int food_width = food_sprite->Width3; + int shield_width = shield_sprite->Width3; + if ((food_width <= 0) && (shield_width > 0)) food_width = shield_width; + if ((shield_width <= 0) && (food_width > 0)) shield_width = food_width; + + int sprite_width = food_width > shield_width ? food_width : shield_width; + int sprite_height = food_sprite->Height; + if (sprite_height == 0) sprite_height = shield_sprite->Height; + if ((sprite_width <= 0) || (sprite_height <= 0)) + return; + + RECT menu_rect = canvas->JGL.Image->Image_Rect; + int menu_width = menu_rect.right - menu_rect.left; + if (menu_width <= 0) + return; + + int icon_left = menu_rect.left + (menu_width >> 1); + int icon_right_edge = menu_rect.right - 6; + int icon_band_width = icon_right_edge - icon_left; + if (icon_band_width < sprite_width) + return; + + int spacing = sprite_width; + if ((total_yield > 1) && (sprite_width * total_yield > icon_band_width)) { + spacing = (icon_band_width - sprite_width) / (total_yield - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; + } + + int used_width = sprite_width + spacing * (total_yield - 1); + int pixel_x = icon_right_edge - used_width; + if (pixel_x < icon_left) + pixel_x = icon_left; + + int pixel_y = row_center_y - (sprite_height >> 1); + if (pixel_y < menu_rect.top) + pixel_y = menu_rect.top; + + for (int i = 0; i < food_yield; i++) { + Sprite_draw (food_sprite, __, canvas, pixel_x + ((sprite_width - food_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < shield_yield; i++) { + Sprite_draw (shield_sprite, __, canvas, pixel_x + ((sprite_width - shield_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } +} + int __fastcall patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { + if (this == &is->distribution_hub_menu_icon_sentinel) { + draw_distribution_hub_menu_icons (canvas, pixel_x, pixel_y); + return 0; + } + Sprite * to_draw = get_cycle_sprite_proxy(this); return Sprite_draw(to_draw ? to_draw : this, __, canvas, pixel_x, pixel_y, color_table); } @@ -26534,11 +26627,11 @@ on_gain_city (Leader * leader, City * city, enum city_gain_reason reason) grant_nearby_wonders_to_city (city); } - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { - refresh_distribution_hubs_for_city (city); - } - } -} + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + refresh_distribution_hubs_for_city (city); + } + } +} void on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) @@ -26556,10 +26649,10 @@ on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) for (int district_id = 0; district_id < is->district_count; district_id++) clear_city_district_request (city, district_id); - if (is->current_config.enable_wonder_districts) - release_wonder_district_reservation (city); - } -} + if (is->current_config.enable_wonder_districts) + release_wonder_district_reservation (city); + } +} // Returns -1 if the location is unusable, 0-9 if it's usable but doesn't satisfy all criteria, and 10 if it couldn't be better int @@ -28511,11 +28604,11 @@ patch_Leader_do_production_phase (Leader * this) auto_dynamic_district_ids[auto_dynamic_district_count++] = CENTRAL_RAIL_HUB_DISTRICT_ID; } - if (is->current_config.enable_distribution_hub_districts) { - if (leader_can_build_district (this, DISTRIBUTION_HUB_DISTRICT_ID)) - ai_update_distribution_hub_goal_for_leader (this); - ai_update_distribution_hub_city_selections_for_leader (this); - } + if (is->current_config.enable_distribution_hub_districts) { + if (leader_can_build_district (this, DISTRIBUTION_HUB_DISTRICT_ID)) + ai_update_distribution_hub_goal_for_leader (this); + ai_update_distribution_hub_city_selections_for_leader (this); + } FOR_CITIES_OF (coi, this->ID) { City * city = coi.city; @@ -33342,6 +33435,126 @@ draw_distribution_hub_yields (City_Form * city_form, Tile * tile, int tile_x, in } } +void __fastcall +patch_Context_Menu_draw_item (Context_Menu * this, int edx, int item_index, int redraw) +{ + Context_Menu_draw_item (this, __, item_index, redraw); + + if ((this == NULL) || + ! is->distribution_hub_menu_active || + (item_index < 0) || + (item_index >= this->Item_Count)) + return; + + Context_Menu_Item * item = &this->Items[item_index]; + if (((item->Status & 1) == 0) || + (item->Menu_Item_ID < DISTRIBUTION_HUB_MENU_CITY_ID_BASE)) + return; + if (item->Image == &is->distribution_hub_menu_icon_sentinel) + return; + + struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); + if (rec == NULL) + return; + + City * city = get_city_ptr (item->Menu_Item_ID - DISTRIBUTION_HUB_MENU_CITY_ID_BASE); + if ((city == NULL) || + ! distribution_hub_distributes_to_city (rec, city)) + return; + + int food_yield = rec->food_yield; + int shield_yield = rec->shield_yield; + int total_yield = food_yield + shield_yield; + if (total_yield <= 0) + return; + + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->distribution_hub_icons_img_state != IS_OK) + return; + + Sprite * food_sprite = &is->distribution_hub_food_icon_small; + Sprite * shield_sprite = &is->distribution_hub_shield_icon_small; + + if (food_sprite->Width3 == 0) food_sprite = &is->distribution_hub_food_icon; + if (shield_sprite->Width3 == 0) shield_sprite = &is->distribution_hub_shield_icon; + + int food_width = food_sprite->Width3; + int shield_width = shield_sprite->Width3; + if ((food_width <= 0) && (shield_width > 0)) food_width = shield_width; + if ((shield_width <= 0) && (food_width > 0)) shield_width = food_width; + + int sprite_width = food_width > shield_width ? food_width : shield_width; + int sprite_height = food_sprite->Height; + if (sprite_height == 0) sprite_height = shield_sprite->Height; + if ((sprite_width <= 0) || (sprite_height <= 0)) + return; + + PCX_Image * canvas = &this->Base.Data.Canvas; + if (canvas->JGL.Image == NULL) + return; + + RECT menu_rect = canvas->JGL.Image->Image_Rect; + int menu_width = menu_rect.right - menu_rect.left; + int menu_height = menu_rect.bottom - menu_rect.top; + int item_height = this->ItemHeight; + if ((menu_width <= 0) || (menu_height <= 0) || (item_height <= 0)) + return; + + int row_index = 0; + int row_count = 0; + for (int n = 0; n < this->Item_Count; n++) + if ((this->Items[n].Status & 1) != 0) { + if (n < item_index) + row_index++; + row_count++; + } + if (row_count <= 0) + return; + + int vertical_pad = (menu_height - row_count * item_height) >> 1; + if (vertical_pad < 0) + vertical_pad = 0; + + int row_top = menu_rect.top + vertical_pad + row_index * item_height; + int pixel_y = row_top + ((item_height - sprite_height) >> 1); + if (pixel_y < menu_rect.top) + pixel_y = menu_rect.top; + + int right_pad = vertical_pad << 1; + if (right_pad < 6) + right_pad = 6; + + int icon_left = menu_rect.left + (menu_width >> 1); + int icon_right_edge = menu_rect.right - right_pad; + int icon_band_width = icon_right_edge - icon_left; + if (icon_band_width < sprite_width) + return; + + int spacing = sprite_width; + if ((total_yield > 1) && (sprite_width * total_yield > icon_band_width)) { + spacing = (icon_band_width - sprite_width) / (total_yield - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; + } + + int used_width = sprite_width + spacing * (total_yield - 1); + int pixel_x = icon_right_edge - used_width; + if (pixel_x < icon_left) + pixel_x = icon_left; + + for (int i = 0; i < food_yield; i++) { + Sprite_draw (food_sprite, __, canvas, pixel_x + ((sprite_width - food_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < shield_yield; i++) { + Sprite_draw (shield_sprite, __, canvas, pixel_x + ((sprite_width - shield_width) >> 1), pixel_y, NULL); + pixel_x += spacing; + } +} + void __fastcall patch_City_Form_draw_yields_on_worked_tiles (City_Form * this) { @@ -35731,12 +35944,12 @@ draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instanc variant = culture; draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); return; - } - case DISTRIBUTION_HUB_DISTRICT_ID: - if (! is->current_config.enable_districts || ! is->current_config.enable_distribution_hub_districts) - return; - - draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + } + case DISTRIBUTION_HUB_DISTRICT_ID: + if (! is->current_config.enable_districts || ! is->current_config.enable_distribution_hub_districts) + return; + + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); return; case ENERGY_GRID_DISTRICT_ID: { @@ -36570,7 +36783,6 @@ recompute_district_and_distribution_hub_shields_for_city_view (City * city) int city_center_base_shields = City_calc_tile_yield_at (city, __, YK_SHIELDS, city_x, city_y); int total_district_shield_bonus = 0; calculate_city_center_district_bonus (city, NULL, &total_district_shield_bonus, NULL); - int city_center_district_shields = total_district_shield_bonus; FOR_DISTRICTS_AROUND (wai, city_x, city_y, true) { int district_id = wai.district_inst->district_id; @@ -36590,14 +36802,12 @@ recompute_district_and_distribution_hub_shields_for_city_view (City * city) total_district_shield_bonus += shield_bonus; } - // Distribution hub contribution is tracked separately for icon rendering. - int distribution_hub_shields = 0; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) - get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); + // Distribution hub contribution is tracked separately for icon rendering. + int distribution_hub_shields = 0; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) + get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); if (distribution_hub_shields < 0) distribution_hub_shields = 0; - if (distribution_hub_shields > city_center_district_shields) - distribution_hub_shields = city_center_district_shields; int standard_district_shields = total_district_shield_bonus - distribution_hub_shields; if (standard_district_shields < 0) From a43df1627b0ac97709ffc879aa40b9cf8c8c0f34 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 14:26:26 -0700 Subject: [PATCH 04/10] Ensure hubs recalculated immediately upon owner change --- injected_code.c | 61 ++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/injected_code.c b/injected_code.c index fc313f7b..83082479 100644 --- a/injected_code.c +++ b/injected_code.c @@ -24896,10 +24896,11 @@ patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool void draw_distribution_hub_menu_icons (PCX_Image * canvas, int image_x, int row_center_y) { - if ((canvas == NULL) || - (canvas->JGL.Image == NULL) || - ! is->distribution_hub_menu_active) - return; + if ((canvas == NULL) || + (canvas->JGL.Image == NULL) || + ! is->distribution_hub_menu_active || + ! distribution_hub_city_select_ui_enabled ()) + return; struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); if (rec == NULL) @@ -24975,10 +24976,12 @@ draw_distribution_hub_menu_icons (PCX_Image * canvas, int image_x, int row_cente int __fastcall patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - if (this == &is->distribution_hub_menu_icon_sentinel) { - draw_distribution_hub_menu_icons (canvas, pixel_x, pixel_y); - return 0; - } + if (this == &is->distribution_hub_menu_icon_sentinel) { + if (is->distribution_hub_menu_active && + distribution_hub_city_select_ui_enabled ()) + draw_distribution_hub_menu_icons (canvas, pixel_x, pixel_y); + return 0; + } Sprite * to_draw = get_cycle_sprite_proxy(this); return Sprite_draw(to_draw ? to_draw : this, __, canvas, pixel_x, pixel_y, color_table); @@ -30641,12 +30644,22 @@ patch_Leader_do_capture_city (Leader * this, int edx, City * city, bool involunt is->current_config.cities_with_mutual_district_receive_wonders && lost_small_wonder_count > 0) { reassign_shared_small_wonder_owners_after_city_loss (previous_owner, lost_small_wonders, lost_small_wonder_count); - } - - on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); - is->currently_capturing_city = NULL; - return tr; -} + } + + on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); + if (is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts) { + int old_civ_id = previous_owner->ID; + int new_civ_id = city->Body.CivID; + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (old_civ_id); + if (new_civ_id != old_civ_id) + recompute_distribution_hub_cities_for_civ (new_civ_id); + } + is->currently_capturing_city = NULL; + return tr; +} void __fastcall patch_City_raze (City * this, int edx, int civ_id_responsible, bool checking_elimination) @@ -31005,10 +31018,11 @@ patch_Context_Menu_get_selected_item_on_unit_rcm (Context_Menu * this) // click unit items which have been disabled by the mod so they can interrupt the queued actions of units that have no moves left. int index = this->Selected_Item; if (index >= 0) { - if (is->distribution_hub_menu_active) { - Context_Menu_Item * item = &this->Items[index]; - if (handle_distribution_hub_menu_selection (item->Menu_Item_ID)) - return -1; + if (is->distribution_hub_menu_active && + distribution_hub_city_select_ui_enabled ()) { + Context_Menu_Item * item = &this->Items[index]; + if (handle_distribution_hub_menu_selection (item->Menu_Item_ID)) + return -1; } if (is->current_config.enable_named_tiles && is->named_tile_menu_active) { Context_Menu_Item * item = &this->Items[index]; @@ -33440,11 +33454,12 @@ patch_Context_Menu_draw_item (Context_Menu * this, int edx, int item_index, int { Context_Menu_draw_item (this, __, item_index, redraw); - if ((this == NULL) || - ! is->distribution_hub_menu_active || - (item_index < 0) || - (item_index >= this->Item_Count)) - return; + if ((this == NULL) || + ! is->distribution_hub_menu_active || + ! distribution_hub_city_select_ui_enabled () || + (item_index < 0) || + (item_index >= this->Item_Count)) + return; Context_Menu_Item * item = &this->Items[item_index]; if (((item->Status & 1) == 0) || From 8d15c077ea6018fbc14439f03a4a8a8d57f7b430 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 15:04:15 -0700 Subject: [PATCH 05/10] Wire context menu draw item for distro hub menu --- civ_prog_objects.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/civ_prog_objects.csv b/civ_prog_objects.csv index b51b05dc..32580bce 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -972,4 +972,4 @@ ignore, 0x4BF660, 0x4C6C10, 0x4BF6F0, "City_draw_citizens", "void (__fastc ignore, 0x4B9F60, 0x4C15D0, 0x4B9FF0, "City_add_population", "void (__fastcall *) (City * this, int edx, int num, int race_id)" ignore, 0x670234, 0x68D2E0, 0x670234, "Tile_m27_Check_Shield_Bonus", "bool (__fastcall *) (Tile * this)" ignore, 0x5f3448, 0x6032DF, 0x5F3378, "CHECK_SHIELD_BONUS_TO_CAN_SPAWN_RES_RETURN", "int" - +inlead, 0x61B320, 0x0, 0x0, "Context_Menu_draw_item", "void (__fastcall *) (Context_Menu * this, int edx, int item_index, int redraw)" From de4ac7beaee96f50924a9616c8bfc4ee99d66509 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 15:05:07 -0700 Subject: [PATCH 06/10] Allow distro hubs to distribute only to select cities --- injected_code.c | 76 ++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/injected_code.c b/injected_code.c index 83082479..39bf1709 100644 --- a/injected_code.c +++ b/injected_code.c @@ -24896,11 +24896,11 @@ patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool void draw_distribution_hub_menu_icons (PCX_Image * canvas, int image_x, int row_center_y) { - if ((canvas == NULL) || - (canvas->JGL.Image == NULL) || - ! is->distribution_hub_menu_active || - ! distribution_hub_city_select_ui_enabled ()) - return; + if ((canvas == NULL) || + (canvas->JGL.Image == NULL) || + ! is->distribution_hub_menu_active || + ! distribution_hub_city_select_ui_enabled ()) + return; struct distribution_hub_record * rec = get_active_distribution_hub_menu_record (); if (rec == NULL) @@ -24976,12 +24976,12 @@ draw_distribution_hub_menu_icons (PCX_Image * canvas, int image_x, int row_cente int __fastcall patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - if (this == &is->distribution_hub_menu_icon_sentinel) { - if (is->distribution_hub_menu_active && - distribution_hub_city_select_ui_enabled ()) - draw_distribution_hub_menu_icons (canvas, pixel_x, pixel_y); - return 0; - } + if (this == &is->distribution_hub_menu_icon_sentinel) { + if (is->distribution_hub_menu_active && + distribution_hub_city_select_ui_enabled ()) + draw_distribution_hub_menu_icons (canvas, pixel_x, pixel_y); + return 0; + } Sprite * to_draw = get_cycle_sprite_proxy(this); return Sprite_draw(to_draw ? to_draw : this, __, canvas, pixel_x, pixel_y, color_table); @@ -30644,22 +30644,22 @@ patch_Leader_do_capture_city (Leader * this, int edx, City * city, bool involunt is->current_config.cities_with_mutual_district_receive_wonders && lost_small_wonder_count > 0) { reassign_shared_small_wonder_owners_after_city_loss (previous_owner, lost_small_wonders, lost_small_wonder_count); - } - - on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); - if (is->current_config.enable_districts && - is->current_config.enable_distribution_hub_districts) { - int old_civ_id = previous_owner->ID; - int new_civ_id = city->Body.CivID; - is->distribution_hub_totals_dirty = true; - recompute_distribution_hub_totals (); - recompute_distribution_hub_cities_for_civ (old_civ_id); - if (new_civ_id != old_civ_id) - recompute_distribution_hub_cities_for_civ (new_civ_id); - } - is->currently_capturing_city = NULL; - return tr; -} + } + + on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); + if (is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts) { + int old_civ_id = previous_owner->ID; + int new_civ_id = city->Body.CivID; + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); + recompute_distribution_hub_cities_for_civ (old_civ_id); + if (new_civ_id != old_civ_id) + recompute_distribution_hub_cities_for_civ (new_civ_id); + } + is->currently_capturing_city = NULL; + return tr; +} void __fastcall patch_City_raze (City * this, int edx, int civ_id_responsible, bool checking_elimination) @@ -31018,11 +31018,11 @@ patch_Context_Menu_get_selected_item_on_unit_rcm (Context_Menu * this) // click unit items which have been disabled by the mod so they can interrupt the queued actions of units that have no moves left. int index = this->Selected_Item; if (index >= 0) { - if (is->distribution_hub_menu_active && - distribution_hub_city_select_ui_enabled ()) { - Context_Menu_Item * item = &this->Items[index]; - if (handle_distribution_hub_menu_selection (item->Menu_Item_ID)) - return -1; + if (is->distribution_hub_menu_active && + distribution_hub_city_select_ui_enabled ()) { + Context_Menu_Item * item = &this->Items[index]; + if (handle_distribution_hub_menu_selection (item->Menu_Item_ID)) + return -1; } if (is->current_config.enable_named_tiles && is->named_tile_menu_active) { Context_Menu_Item * item = &this->Items[index]; @@ -33454,12 +33454,12 @@ patch_Context_Menu_draw_item (Context_Menu * this, int edx, int item_index, int { Context_Menu_draw_item (this, __, item_index, redraw); - if ((this == NULL) || - ! is->distribution_hub_menu_active || - ! distribution_hub_city_select_ui_enabled () || - (item_index < 0) || - (item_index >= this->Item_Count)) - return; + if ((this == NULL) || + ! is->distribution_hub_menu_active || + ! distribution_hub_city_select_ui_enabled () || + (item_index < 0) || + (item_index >= this->Item_Count)) + return; Context_Menu_Item * item = &this->Items[item_index]; if (((item->Status & 1) == 0) || From a10c38c2577ab9c96e27cf147c3599c3c3e101e4 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 15:20:06 -0700 Subject: [PATCH 07/10] Update distro hub scaling scores --- injected_code.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/injected_code.c b/injected_code.c index 39bf1709..f012cffa 100644 --- a/injected_code.c +++ b/injected_code.c @@ -7277,16 +7277,22 @@ recompute_distribution_hub_yields (struct distribution_hub_record * rec) rec->shield_yield = 0; return; } - int city_root = 1; - while ((city_root + 1) * (city_root + 1) <= connected_city_count) - city_root++; - int city_food_divisor = city_root * food_div; - int city_shield_divisor = city_root * shield_div; - if (city_food_divisor < 1) city_food_divisor = 1; - if (city_shield_divisor < 1) city_shield_divisor = 1; - rec->food_yield = food_sum / city_food_divisor; - rec->shield_yield = shield_sum / city_shield_divisor; - } else { + int scaled_city_count = connected_city_count * 16; // quarter-step sqrt scale + int city_root_x4 = 4; + while ((city_root_x4 + 1) * (city_root_x4 + 1) <= scaled_city_count) + city_root_x4++; + int next_city_root_x4 = city_root_x4 + 1; + int lower_delta = scaled_city_count - city_root_x4 * city_root_x4; + int upper_delta = next_city_root_x4 * next_city_root_x4 - scaled_city_count; + if (upper_delta < lower_delta) + city_root_x4 = next_city_root_x4; + int city_food_divisor = city_root_x4 * food_div; + int city_shield_divisor = city_root_x4 * shield_div; + if (city_food_divisor < 1) city_food_divisor = 1; + if (city_shield_divisor < 1) city_shield_divisor = 1; + rec->food_yield = (food_sum * 4 + (city_food_divisor >> 1)) / city_food_divisor; + rec->shield_yield = (shield_sum * 4 + (city_shield_divisor >> 1)) / city_shield_divisor; + } else { rec->food_yield = food_sum / food_div; rec->shield_yield = shield_sum / shield_div; } From 24f55a9b9060c08810b84e0df3e43eddc916d959 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 15:21:36 -0700 Subject: [PATCH 08/10] Update distro hub docs --- default.c3x_config.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default.c3x_config.ini b/default.c3x_config.ini index d65fc6a0..2e6e613f 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -1107,7 +1107,7 @@ destroyed_wonders_can_be_built_again = true ; Distribution Hubs work as "breadbaskets" and mining areas far from urban centers, minimizing local city potential but benefiting the entire civilization. ; Distribution Hubs make surrounding tiles unworkable and instead distribute their raw food and shield yields to ALL connected cities in your civilization. -; "Divisors" for food and shields multiply the sqrt-based divisor in scale-by-city-count mode, and are a straight divider in flat mode. Yields are subject +; "Divisors" for food and shields multiply the quarter-step sqrt-based divisor in scale-by-city-count mode, and are a straight divider in flat mode. Yields are subject ; to corruption as with regular shields. ; ; ai_ideal_distribution_hub_count_per_100_cities controls how many hubs the AI tries to maintain per 100 cities if distribution_hub_yield_division_mode @@ -1115,7 +1115,7 @@ destroyed_wonders_can_be_built_again = true ; ; distribution_hub_yield_division_mode controls how a hub splits its collected food/shields across connected cities: ; flat: Divide raw yields by the configured divisors (distribution_hub_food_yield_divisor & distribution_hub_shield_yield_divisor) -; scale-by-city-count: Bonuses gradually reduce as more cities plug into the hub. Formula: floor(raw_yield / (sqrt(connected_city_count) * divisor)) +; scale-by-city-count: Bonuses gradually reduce as more cities plug into the hub. Formula: round(raw_yield / (round_sqrt_to_nearest_quarter(connected_city_count) * divisor)) ; ; ai_distribution_hub_build_strategy controls how the AI decides to build distribution hubs: ; by-city-count: AI builds hubs based on its ideal hub count per 100 cities From ad8fb71559c1279a64c0b8f759ddefecf88037b7 Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 18:43:26 -0700 Subject: [PATCH 09/10] Make distro hub city selection language dynamic --- C3X.h | 16 ++++++++++------ Text/c3x-labels.txt | 16 ++++++++++------ injected_code.c | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/C3X.h b/C3X.h index 3fae44a7..364dcad1 100644 --- a/C3X.h +++ b/C3X.h @@ -676,12 +676,16 @@ enum c3x_label { CL_DISTRICTS_IN_SAVE_FILE, CL_CURRENTLY_CONFIGURED_DISTRICTS, - // Tile naming - CL_NAME_TILE, - CL_RENAME_TILE, - - // "Action" for passenger units - CL_TRANSPORTED, + // Tile naming + CL_NAME_TILE, + CL_RENAME_TILE, + + // Distribution hub right-click menu + CL_DISTRIBUTE_TO_ALL_CITIES, + CL_DISTRIBUTE_TO_SPECIFIC_CITIES, + + // "Action" for passenger units + CL_TRANSPORTED, CL_IN_STATE_27, CL_IN_STATE_28, diff --git a/Text/c3x-labels.txt b/Text/c3x-labels.txt index 80d84e88..817f841a 100644 --- a/Text/c3x-labels.txt +++ b/Text/c3x-labels.txt @@ -140,12 +140,16 @@ There may be other errors as well. Districts in save file Currently configured districts from -; For naming tiles -Name Tile -Rename - -; This appears instead of the above actions on units that have been loaded into another (except an army). -Transported +; For naming tiles +Name Tile +Rename + +; Distribution hub right-click menu +Distribute to All Cities +Distribute to Specific Cities + +; This appears instead of the above actions on units that have been loaded into another (except an army). +Transported ; Action labels for the four unknown unit states. These labels are placeholders that should never actually appear on the UI. In State 27 diff --git a/injected_code.c b/injected_code.c index f012cffa..9998933f 100644 --- a/injected_code.c +++ b/injected_code.c @@ -24342,16 +24342,16 @@ add_distribution_hub_menu_items (Context_Menu * menu, struct distribution_hub_re if ((menu == NULL) || (rec == NULL)) return; - if (menu->Item_Count > 0) - Context_Menu_add_separator (menu, __, 0); - - bool specific = rec->city_selection_mode == DHCSM_SPECIFIC_CITIES; - Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID, "Distribute to All Cities", false, (Sprite *)0x0); - if (specific) - Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID); - Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID, "Distribute to Specific Cities", false, (Sprite *)0x0); - if (! specific) - Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID); + if (menu->Item_Count > 0) + Context_Menu_add_separator (menu, __, 0); + + bool specific = rec->city_selection_mode == DHCSM_SPECIFIC_CITIES; + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID, is->c3x_labels[CL_DISTRIBUTE_TO_ALL_CITIES], false, (Sprite *)0x0); + if (specific) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_ALL_ID); + Context_Menu_add_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID, is->c3x_labels[CL_DISTRIBUTE_TO_SPECIFIC_CITIES], false, (Sprite *)0x0); + if (! specific) + Context_Menu_disable_item (menu, __, DISTRIBUTION_HUB_MENU_SPECIFIC_ID); Context_Menu_add_separator (menu, __, 0); FOR_CITIES_OF (coi, rec->civ_id) { From 43c27fa39023075efca2173efe279a21ad45dece Mon Sep 17 00:00:00 2001 From: Instafluff Date: Sat, 27 Jun 2026 18:57:28 -0700 Subject: [PATCH 10/10] Gate distro hub city selection behind flag --- C3X.h | 9 ++++---- default.c3x_config.ini | 22 ++++++++++++-------- injected_code.c | 47 +++++++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/C3X.h b/C3X.h index 364dcad1..f52329b2 100644 --- a/C3X.h +++ b/C3X.h @@ -507,10 +507,11 @@ struct c3x_config { bool completed_wonder_districts_can_be_destroyed; bool destroyed_wonders_can_be_built_again; - int distribution_hub_yield_division_mode; - int distribution_hub_food_yield_divisor; - int distribution_hub_shield_yield_divisor; - int ai_distribution_hub_build_strategy; + int distribution_hub_yield_division_mode; + bool enable_distribution_hub_city_selection; + int distribution_hub_food_yield_divisor; + int distribution_hub_shield_yield_divisor; + int ai_distribution_hub_build_strategy; int ai_ideal_distribution_hub_count_per_100_cities; int max_distribution_hub_count_per_100_cities; int central_rail_hub_distribution_food_bonus_percent; diff --git a/default.c3x_config.ini b/default.c3x_config.ini index 2e6e613f..a3ee5698 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -1113,16 +1113,20 @@ destroyed_wonders_can_be_built_again = true ; ai_ideal_distribution_hub_count_per_100_cities controls how many hubs the AI tries to maintain per 100 cities if distribution_hub_yield_division_mode ; is set to "flat" (e.g., 25 means the AI aims for 1 hub per 4 cities). ; -; distribution_hub_yield_division_mode controls how a hub splits its collected food/shields across connected cities: -; flat: Divide raw yields by the configured divisors (distribution_hub_food_yield_divisor & distribution_hub_shield_yield_divisor) +; distribution_hub_yield_division_mode controls how a hub splits its collected food/shields across connected cities: +; flat: Divide raw yields by the configured divisors (distribution_hub_food_yield_divisor & distribution_hub_shield_yield_divisor) ; scale-by-city-count: Bonuses gradually reduce as more cities plug into the hub. Formula: round(raw_yield / (round_sqrt_to_nearest_quarter(connected_city_count) * divisor)) -; -; ai_distribution_hub_build_strategy controls how the AI decides to build distribution hubs: -; by-city-count: AI builds hubs based on its ideal hub count per 100 cities -; auto: AI dynamically assesses need based on city growth and food/shield deficits across civ -distribution_hub_yield_division_mode = scale-by-city-count -ai_distribution_hub_build_strategy = auto -distribution_hub_food_yield_divisor = 2 +; +; enable_distribution_hub_city_selection lets human players and AIs choose specific connected cities for hub distribution. If false, hubs always +; distribute to all connected cities. +; +; ai_distribution_hub_build_strategy controls how the AI decides to build distribution hubs: +; by-city-count: AI builds hubs based on its ideal hub count per 100 cities +; auto: AI dynamically assesses need based on city growth and food/shield deficits across civ +distribution_hub_yield_division_mode = scale-by-city-count +enable_distribution_hub_city_selection = true +ai_distribution_hub_build_strategy = auto +distribution_hub_food_yield_divisor = 2 distribution_hub_shield_yield_divisor = 2 ai_ideal_distribution_hub_count_per_100_cities = 50 max_distribution_hub_count_per_100_cities = 50 diff --git a/injected_code.c b/injected_code.c index 9998933f..5befe7e7 100644 --- a/injected_code.c +++ b/injected_code.c @@ -6875,9 +6875,10 @@ distribution_hub_distributes_to_city (struct distribution_hub_record * rec, City if (! distribution_hub_accessible_to_city (rec, city)) return false; - if ((is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) && - (rec->city_selection_mode == DHCSM_SPECIFIC_CITIES)) - return distribution_hub_city_is_selected (rec, city); + if (is->current_config.enable_distribution_hub_city_selection && + (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) && + (rec->city_selection_mode == DHCSM_SPECIFIC_CITIES)) + return distribution_hub_city_is_selected (rec, city); return true; } @@ -6961,11 +6962,12 @@ ai_score_distribution_hub_target_city (City * city) bool ai_update_distribution_hub_city_selection (struct distribution_hub_record * rec) { - if ((rec == NULL) || - ! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) - return false; + if ((rec == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + ! is->current_config.enable_distribution_hub_city_selection || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return false; int civ_id = rec->civ_id; if ((civ_id < 0) || (civ_id >= 32)) @@ -7026,11 +7028,12 @@ ai_update_distribution_hub_city_selection (struct distribution_hub_record * rec) void ai_update_distribution_hub_city_selections_for_leader (Leader * leader) { - if ((leader == NULL) || - ! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) - return; + if ((leader == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + ! is->current_config.enable_distribution_hub_city_selection || + (is->current_config.distribution_hub_yield_division_mode != DHYDM_SCALE_BY_CITY_COUNT)) + return; int civ_id = leader->ID; if ((civ_id < 0) || (civ_id >= 32)) @@ -18669,10 +18672,11 @@ patch_init_floating_point () {"enable_neighborhood_districts" , false, offsetof (struct c3x_config, enable_neighborhood_districts)}, {"enable_wonder_districts" , false, offsetof (struct c3x_config, enable_wonder_districts)}, {"enable_natural_wonders" , false, offsetof (struct c3x_config, enable_natural_wonders)}, - {"add_natural_wonders_to_scenarios_if_none" , false, offsetof (struct c3x_config, add_natural_wonders_to_scenarios_if_none)}, - {"enable_named_tiles" , false, offsetof (struct c3x_config, enable_named_tiles)}, - {"enable_distribution_hub_districts" , false, offsetof (struct c3x_config, enable_distribution_hub_districts)}, - {"enable_aerodrome_districts" , false, offsetof (struct c3x_config, enable_aerodrome_districts)}, + {"add_natural_wonders_to_scenarios_if_none" , false, offsetof (struct c3x_config, add_natural_wonders_to_scenarios_if_none)}, + {"enable_named_tiles" , false, offsetof (struct c3x_config, enable_named_tiles)}, + {"enable_distribution_hub_districts" , false, offsetof (struct c3x_config, enable_distribution_hub_districts)}, + {"enable_distribution_hub_city_selection" , true , offsetof (struct c3x_config, enable_distribution_hub_city_selection)}, + {"enable_aerodrome_districts" , false, offsetof (struct c3x_config, enable_aerodrome_districts)}, {"enable_port_districts" , false, offsetof (struct c3x_config, enable_port_districts)}, {"enable_bridge_districts" , false, offsetof (struct c3x_config, enable_bridge_districts)}, {"enable_canal_districts" , false, offsetof (struct c3x_config, enable_canal_districts)}, @@ -24279,10 +24283,11 @@ patch_Context_Menu_add_item_and_set_color (Context_Menu * this, int edx, int ite bool distribution_hub_city_select_ui_enabled (void) { - return is->current_config.enable_districts && - is->current_config.enable_distribution_hub_districts && - (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT); -} + return is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts && + is->current_config.enable_distribution_hub_city_selection && + (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT); +} struct distribution_hub_record * get_active_distribution_hub_menu_record (void)