diff --git a/C3X.h b/C3X.h index 66f5af79..bbf3ac29 100644 --- a/C3X.h +++ b/C3X.h @@ -137,6 +137,76 @@ enum perfume_kind { COUNT_PERFUME_KINDS }; +enum map_target_type { + CITY,//flag foreign, flag population, flag culture + TERRAIN,//flag food_yield, flag shield_yield, flag commerce_yield + RESOURCE,//flag + UNIT,//flag foreign, flag hostile +}; + +struct map_target_city { + int flags; + bool foreign;//flag 1 + bool hostile;//flag 2 + int population_min;//flag 4 + int population_max;//flag 8 + int culture_min;//flag 16 + int culture_max;//flag 32 +}; + +struct map_target_terrain { + int flags; + int type;//flag 1 + int food_yield_min;//flag 2 + int food_yield_max;//flag 4 + int shield_yield_min;//flag 8 + int shield_yield_max;//flag 16 + int commerce_yield_min;//flag 32 + int commerce_yield_max;//flag 64 +}; + +struct map_target_resource { + int flags; + int type;//flag 1 + bool is_visible;//flag 2 +}; + +struct map_target_unit { + int flags; + int type;//flag 1 + bool foreign;//flag 2 + bool hostile;//flag 4 +}; + +struct map_target { + enum map_target_type type; + //Could use a union in theory, but size isn't too important here + struct map_target_city map_target_city; + struct map_target_terrain map_target_terrain; + struct map_target_resource map_target_resource; + struct map_target_unit map_target_unit; +}; + +struct map_target_separation_rule { + struct map_target target; + int min_count; + int max_count; + int enable_tech; + int disable_tech; + + int distance_metric_flags; + int chebyshev;//flag 1 + int manhatten;//flag 2 + int euclidean_percent;//flag 4 +}; + +struct minimum_city_separation { + int any_chebyshev; + int any_manhatten; + int foreign_chebyshev; + int foreign_manhatten; +}; + struct c3x_config { bool enable_stack_bombard; bool enable_disorder_warning; @@ -150,8 +220,9 @@ struct c3x_config { bool enable_stack_unit_commands; bool skip_repeated_tile_improv_replacement_asks; bool autofill_best_gold_amount_when_trading; - int minimum_city_separation; - bool disallow_founding_next_to_foreign_city; + struct minimum_city_separation minimum_city_separation; + struct map_target_separation_rule * minimum_city_separation_rules; + int count_minimum_city_separation_rules; bool enable_trade_screen_scroll; bool group_units_on_right_click_menu; bool gray_out_units_on_menu_with_no_remaining_moves; diff --git a/default.c3x_config.ini b/default.c3x_config.ini index 42eacf65..27baa318 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -349,13 +349,20 @@ remove_era_limit = false prevent_autorazing = false prevent_razing_by_players = false -; These options allow you to adjust the minimum allowed distance between cities. minimum_city_separation controls the minimum number of tiles between -; cities so, e.g, if it's set to 1, cities cannot be founded adjacent to one another, there must be at least one open tile separating them. A setting -; of 1 there corresponds to the standard game rules. disallow_founding_next_to_foreign_city is an optional additional restriction. If it's enabled, -; cities may not be founded adjacent to a city belonging to another civ regardless of the minimum separation. It is only relevant when the minimum -; separation is set to zero. -minimum_city_separation = 1 -disallow_founding_next_to_foreign_city = true +; These options allow you to adjust the minimum allowed distance between cities. minimum_city_separation_chebyshev controls the minimum number of tiles +; between cities so, e.g, if it's set to 1, cities cannot be founded adjacent to one another, there must be at least one open tile separating them. A +; setting of 1 there corresponds to the standard game rules. +; minimum_city_separation_manhatten does the same thing, but uses manhatten distance. The clearance is a diamond shape instead of a square (or the +; converse if viewed on the isometric map.) As a rule of thumb for disabling this, set it to 2x the chebyshev distance or more. +; Both rules are defined at the same time, so if either distance is enough, the city may be built. This means the clearance area around a city can be +; an octagon (as opposed to a star shape) and you may need to adjust both settings to achieve your desired result. +; Finally, an additional pair of configs are given to define an equivilent clearance area between cities from different civs. Foreign cities are still +; subject to the regular chebyshev and manhatten distance rules, but this means if for example you have a clearance of 0, you may require foreign cities +; to still be placed at a distance of 1 away. +minimum_city_separation_chebyshev = 1 +minimum_city_separation_manhatten = 2 +minimum_foreign_city_separation_chebyshev = 1 +minimum_foreign_city_separation_manhatten = 2 ; Set to a number to limit railroad movement to that many tiles per turn. To return to infinite railroad movement, set to false or a number <=0. By ; default, all units travel the same distance along limited rails like in Civ 4, but this can be changed by toggling the next option below. diff --git a/injected_code.c b/injected_code.c index 89e17abd..d09edf37 100644 --- a/injected_code.c +++ b/injected_code.c @@ -1359,6 +1359,130 @@ parse_work_area_improvement (char ** p_cursor, struct error_line ** p_unrecogniz return RPR_PARSE_ERROR; } +bool +parse_map_target_city (char ** p_cursor, struct map_target_city * target) +{ + char * cur = *p_cursor; + if (!skip_white_space (&cur)) + return false; + struct string_slice condition_name; + target->flags = 0; + while (parse_string (&cur, &condition_name)) { + if (slice_matches_str(&condition_name, "foreign")) { + target->flags |= 1; + target->foreign = true; + } else if (slice_matches_str(&condition_name, "hostile")) { + target->flags |= 2; + target->hostile = true; + } else if (slice_matches_str(&condition_name, "population")) { + if (!skip_white_space (&cur)) + return false; + int num; + if (!skip_white_space (&cur) || !parse_int (&cur, &num)) + return false; + target->flags |= 4; + target->population_min = num; + if (skip_white_space (&cur) && skip_punctuation(&cur, '-')) { + if (skip_white_space(&cur) && parse_int (&cur, &num)) { + target->flags |= 8; + target->population_max = num; + } else { + return false; + } + } + } else if (slice_matches_str(&condition_name, "culture")) { + if (!skip_white_space (&cur)) + return false; + int num; + if (!skip_white_space (&cur) || !parse_int (&cur, &num)) + return false; + target->flags |= 16; + target->culture_min = num; + if (skip_white_space (&cur) && skip_punctuation(&cur, '-')) { + if (skip_white_space(&cur) && parse_int (&cur, &num)) { + target->flags |= 32; + target->culture_max = num; + } else { + return false; + } + } + } else { + return false; + } + } + return true; + +} + +bool +parse_map_target (char ** p_cursor, struct map_target * target) +{ + char * cur = *p_cursor; + struct string_slice target_type; + if (!parse_string (&cur, &target_type)) + return false; + if (slice_matches_str (&target_type, "city")) { + target->type = CITY; + return parse_map_target_city (p_cursor, (struct map_target_city *) (target + offsetof (struct map_target, map_target_city))); + } else if (slice_matches_str (&target_type, "terrain")) { + target->type = TERRAIN; + //unimpl + return false; + } else if (slice_matches_str (&target_type, "resource")) { + target->type = RESOURCE; + //unimpl + return false; + } else if (slice_matches_str (&target_type, "unit")) { + target->type = UNIT; + //unimpl + return false; + } else { + return false; + } +} + +enum recognizable_parse_result +parse_city_separation_rule (char ** p_cursor, struct error_line ** p_unrecognized_lines, void * out_parsed_city_separation_rule) +{ + char * cur = *p_cursor; + struct map_target_separation_rule * out = out_parsed_city_separation_rule; + + struct string_slice target_type; + if (skip_white_space (&cur) && + parse_map_target (&cur, (struct map_target *) (out + offsetof (struct map_target_separation_rule, target))) && + skip_punctuation (&cur, ':')) { + + struct string_slice and_separator; + do { + int num; + if (!skip_white_space (&cur) || !parse_int (&cur, &num)) + return RPR_PARSE_ERROR; + + out->distance_metric_flags = 0; + + struct string_slice metric_type; + if (parse_string (&cur, &metric_type)) { + if (slice_matches_str (&metric_type, "chebychev")) { + out->chebyshev = num; + out->distance_metric_flags |= 1; + } else if (slice_matches_str (&metric_type, "manhatten")) { + out->manhatten = num; + out->distance_metric_flags |= 2; + } else if (slice_matches_str (&metric_type, "euclidean_percent")) { + out->euclidean_percent = num; + out->distance_metric_flags |= 4; + } else + return RPR_PARSE_ERROR; + } else {//Default to chebyshev if nothing stated. + out->chebyshev = num; + out->distance_metric_flags |= 1; + } + } while (skip_white_space (&cur) && parse_string(&cur, &and_separator) && slice_matches_str(&and_separator, "and")); + return RPR_OK; + } else + return RPR_PARSE_ERROR; +} + // Recognizable items are appended to out_list/count, which must have been previously initialized (NULL/0 is valid for an empty list). // If an error occurs while reading, returns the location of the error inside the slice, specifically the number of characters before the unreadable // item. If no error occurs, returns -1. @@ -2117,9 +2241,24 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) cfg->anarchy_length_percent = 100 - ival; else handle_config_error (&p, CPE_BAD_INT_VALUE); - } else if (slice_matches_str (&p.key, "adjust_minimum_city_separation")) { + } else if (slice_matches_str (&p.key, "minimum_city_separation_chebyshev") || slice_matches_str (&p.key, "adjust_minimum_city_separation")) { + if (read_int (&value, &ival)) + cfg->minimum_city_separation.any_chebyshev = ival; + else + handle_config_error (&p, CPE_BAD_INT_VALUE); + } else if (slice_matches_str (&p.key, "minimum_city_separation_manhatten")) { + if (read_int (&value, &ival)) + cfg->minimum_city_separation.any_manhatten = ival; + else + handle_config_error (&p, CPE_BAD_INT_VALUE); + } else if (slice_matches_str (&p.key, "minimum_foreign_city_separation_chebyshev")) { + if (read_int (&value, &ival)) + cfg->minimum_city_separation.foreign_chebyshev = ival; + else + handle_config_error (&p, CPE_BAD_INT_VALUE); + } else if (slice_matches_str (&p.key, "minimum_foreign_city_separation_manhatten")) { if (read_int (&value, &ival)) - cfg->minimum_city_separation = ival + 1; + cfg->minimum_city_separation.foreign_manhatten = ival; else handle_config_error (&p, CPE_BAD_INT_VALUE); } else if (slice_matches_str (&p.key, "reduce_max_escorts_per_ai_transport")) { @@ -10641,7 +10780,6 @@ patch_init_floating_point () {"enable_stack_unit_commands" , true , offsetof (struct c3x_config, enable_stack_unit_commands)}, {"skip_repeated_tile_improv_replacement_asks" , true , offsetof (struct c3x_config, skip_repeated_tile_improv_replacement_asks)}, {"autofill_best_gold_amount_when_trading" , true , offsetof (struct c3x_config, autofill_best_gold_amount_when_trading)}, - {"disallow_founding_next_to_foreign_city" , true , offsetof (struct c3x_config, disallow_founding_next_to_foreign_city)}, {"enable_trade_screen_scroll" , true , offsetof (struct c3x_config, enable_trade_screen_scroll)}, {"group_units_on_right_click_menu" , true , offsetof (struct c3x_config, group_units_on_right_click_menu)}, {"gray_out_units_on_menu_with_no_remaining_moves" , true , offsetof (struct c3x_config, gray_out_units_on_menu_with_no_remaining_moves)}, @@ -10776,7 +10914,10 @@ patch_init_floating_point () int offset; } integer_config_options[] = { {"limit_railroad_movement" , 0, offsetof (struct c3x_config, limit_railroad_movement)}, - {"minimum_city_separation" , 1, offsetof (struct c3x_config, minimum_city_separation)}, + {"minimum_city_separation_chebychev" , 1, offsetof (struct c3x_config, minimum_city_separation.any_chebyshev)}, + {"minimum_city_separation_manhatten" , 2, offsetof (struct c3x_config, minimum_city_separation.any_manhatten)}, + {"minimum_foreign_city_separation_chebychev" , 1, offsetof (struct c3x_config, minimum_city_separation.foreign_chebyshev)}, + {"minimum_foreign_city_separation_manhatten" , 2, offsetof (struct c3x_config, minimum_city_separation.foreign_manhatten)}, {"anarchy_length_percent" , 100, offsetof (struct c3x_config, anarchy_length_percent)}, {"ai_multi_city_start" , 0, offsetof (struct c3x_config, ai_multi_city_start)}, {"max_tries_to_place_fp_city" , 10000, offsetof (struct c3x_config, max_tries_to_place_fp_city)}, @@ -12226,7 +12367,7 @@ patch_Main_GUI_set_up_unit_command_buttons (Main_GUI * this) } // If the minimum city separation is increased, then gray out the found city button if we're too close to another city. - if ((is->current_config.minimum_city_separation > 1) && (p_main_screen_form->Current_Unit != NULL) && (is->disabled_command_img_state == IS_OK)) { + if ((p_main_screen_form->Current_Unit != NULL) && (is->disabled_command_img_state == IS_OK)) { Unit_Body * selected_unit = &p_main_screen_form->Current_Unit->Body; // For each unit command button @@ -12250,7 +12391,7 @@ patch_Main_GUI_set_up_unit_command_buttons (Main_GUI * this) * to_replace = "$NUM0", * replace_location = strstr (label, to_replace); if (replace_location != NULL) - snprintf (tooltip, sizeof tooltip, "%.*s%d%s", replace_location - label, label, is->current_config.minimum_city_separation, replace_location + strlen (to_replace)); + snprintf (tooltip, sizeof tooltip, "%.*s%d%s", replace_location - label, label, is->current_config.minimum_city_separation.any_chebyshev, replace_location + strlen (to_replace)); else snprintf (tooltip, sizeof tooltip, "%s", label); tooltip[(sizeof tooltip) - 1] = '\0'; @@ -12660,7 +12801,7 @@ patch_Unit_can_do_worker_command_for_button_setup (Unit * this, int edx, int uni // grayed out button image is initialized now so we don't activate the build city button then find out later we can't gray it out. if ((! base) && (unit_command_value == UCV_Build_City) && - (is->current_config.minimum_city_separation > 1) && + //Shortcut removed here due to manhatten dist addition (patch_Map_check_city_location (&p_bic_data->Map, __, this->Body.X, this->Body.Y, this->Body.CivID, false) == CLV_CITY_TOO_CLOSE) && (init_disabled_command_buttons (), is->disabled_command_img_state == IS_OK)) return true; @@ -15153,6 +15294,49 @@ patch_PopupForm_set_text_key_and_flags (PopupForm * this, int edx, char * script PopupForm_set_text_key_and_flags (this, __, script_path, text_key, param_3, param_4, param_5, param_6); } +bool +match_target_city (struct map_target_city * target, City * city, int civ_id) +{ + if (city == NULL) + return false; + if (target->flags & 1 != 0 && city->Body.CivID == civ_id) + return false; + if (target->flags & 2 != 0) + return false; //war check not implemented + if (target->flags & 4 != 0 && city->Body.Population.Size < target->population_min) + return false; + if (target->flags & 8 != 0 && city->Body.Population.Size > target->population_max) + return false; + if (target->flags & 16 != 0) + return false; //culture check not implemented + if (target->flags & 32 != 0) + return false; //culture check not implemented + return true; +} +bool +match_target(struct map_target * target, int tile_x, int tile_y, int civ_id) +{ + if (target->type == CITY) { + City * city = city_at(tile_x, tile_y); + return match_target_city((struct map_target_city *)(target + offsetof (struct map_target, map_target_city)), city, civ_id); + } else { + //Not implemented + return false; + } +} + +bool +map_target_separation_rule_active(struct map_target_separation_rule * rule, int civ_id) +{ + if (rule == NULL) + return false; + if (rule->enable_tech != 0 && !Leader_has_tech (&leaders[civ_id], __, rule->enable_tech) + return false; + if (rule->disable_tech != 0 && Leader_has_tech (&leaders[civ_id], __, rule->disable_tech)) + return false; + return true; +} + CityLocValidity __fastcall patch_Map_check_city_location (Map *this, int edx, int tile_x, int tile_y, int civ_id, bool check_for_city_on_tile) { @@ -15167,67 +15351,87 @@ patch_Map_check_city_location (Map *this, int edx, int tile_x, int tile_y, int c } } - int min_sep = is->current_config.minimum_city_separation; CityLocValidity base_result = Map_check_city_location (this, __, tile_x, tile_y, civ_id, check_for_city_on_tile); - - // If minimum separation is one, make no change - if (min_sep == 1) + if (base_result != CLV_OK && base_result != CLV_CITY_TOO_CLOSE) return base_result; + + struct map_target_separation_rule * city_separation_rules = is->current_config.minimum_city_separation_rules; + //init array of bools for requirements check + //dislike reallocating each time, but injected_state is a pain and I also don't want to bleed state into the config structs. + int * rule_matches = malloc (sizeof(int) * is->current_config.count_minimum_city_separation_rules); + + //init bounding box + int min_sep_chebyshev = 0; + int min_sep_manhatten = 0; + int min_sep_euclidean_percent = 0; + for (int i = 0; i < is->current_config.count_minimum_city_separation_rules; i++) { + struct map_target_separation_rule * current_rule = city_separation_rules + i * sizeof(struct map_target_separation_rule); + if (!map_target_separation_rule_active(current_rule, civ_id)) + continue; + if (current_rule->distance_metric_flags & 1 != 0 && current_rule->chebyshev > min_sep_chebyshev) + min_sep_chebyshev = current_rule->chebyshev; + if (current_rule->distance_metric_flags & 2 != 0 && current_rule->manhatten > min_sep_manhatten) + min_sep_manhatten = current_rule->manhatten; + if (current_rule->distance_metric_flags & 4 != 0 && current_rule->euclidean_percent > min_sep_euclidean_percent) + min_sep_euclidean_percent = current_rule->euclidean_percent; + } + int min_sep_box = min_sep_chebyshev; + if (min_sep_manhatten > min_sep_box) + min_sep_box = min_sep_manhatten; + if ((min_sep_euclidean_percent + 99) / 100 > min_sep_box) + min_sep_box = (min_sep_euclidean_percent + 99) / 100; + + //Otherwise perform calculation ourselves + //start calcing dx/dy at 45 degrees / on a virtual grid + for (int dx = -min_sep_box; dx <= min_sep_box; dx++) { + for (int dy = -min_sep_box; dy <= min_sep_box; dy++) { + int absx = int_abs(dx); + int absy = int_abs(dy); + int chebyshev = absx > absy ? absx : absy; + int manhatten = absx + absy; + int euclidean_percent_squared = (dx*dx) + (dy*dy) * 10000; + //transform to map coords + int tx = tile_x + dx-dy; + int ty = tile_y + dy+dx; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); - // If minimum separation is <= 0, ignore the CITY_TOO_CLOSE objection to city placement unless the location is next to a city belonging to - // another civ and the settings forbid founding there. - else if ((min_sep <= 0) && (base_result == CLV_CITY_TOO_CLOSE)) { - if (is->current_config.disallow_founding_next_to_foreign_city) - for (int n = 1; n <= 8; n++) { - int x, y; - get_neighbor_coords (&p_bic_data->Map, tile_x, tile_y, n, &x, &y); - City * city = city_at (x, y); - if ((city != NULL) && (city->Body.CivID != civ_id)) - return CLV_CITY_TOO_CLOSE; - } - return CLV_OK; - - // If we have an increased separation we might have to exclude some locations the base code allows. - } else if ((min_sep > 1) && (base_result == CLV_OK)) { - // Check tiles around (x, y) for a city. Because the base result is CLV_OK, we don't have to check neighboring tiles, just those at - // distance 2, 3, ... up to (an including) the minimum separation - for (int dist = 2; dist <= min_sep; dist++) { - - // vertices stores the unwrapped coords of the tiles at the vertices of the square of tiles at distance "dist" around - // (tile_x, tile_y). The order of the vertices is north, east, south, west. - struct vertex { - int x, y; - } vertices[4] = { - {tile_x , tile_y - 2*dist}, - {tile_x + 2*dist, tile_y }, - {tile_x , tile_y + 2*dist}, - {tile_x - 2*dist, tile_y } - }; - - // neighbor index for direction of tiles along edge starting from each vertex - // values correspond to directions: southeast, southwest, northwest, northeast - int edge_dirs[4] = {3, 5, 7, 1}; - - // Loop over verts and check tiles along their associated edges. The N vert is associated with the NE edge, the E vert with - // the SE edge, etc. - for (int vert = 0; vert < 4; vert++) { - wrap_tile_coords (&p_bic_data->Map, &vertices[vert].x, &vertices[vert].y); - int dx, dy; - neighbor_index_to_diff (edge_dirs[vert], &dx, &dy); - for (int j = 0; j < 2*dist; j++) { // loop over tiles along this edge - int cx = vertices[vert].x + j * dx, - cy = vertices[vert].y + j * dy; - wrap_tile_coords (&p_bic_data->Map, &cx, &cy); - if (city_at (cx, cy)) - return CLV_CITY_TOO_CLOSE; + //Now check each rule (order things this way to not recalculate positions and distances... is this even sensible?) + for (int i = 0; i < is->current_config.count_minimum_city_separation_rules; i++) { + struct map_target_separation_rule * current_rule = city_separation_rules + i * sizeof(struct map_target_separation_rule); + if (!map_target_separation_rule_active(current_rule, civ_id)) + continue; + //Check tile is within rule's radius + if (current_rule->distance_metric_flags & 1 != 0 && current_rule->chebyshev <= chebyshev) + continue; + if (current_rule->distance_metric_flags & 2 != 0 && current_rule->manhatten <= manhatten) + continue; + if (current_rule->distance_metric_flags & 4 != 0 && (current_rule->euclidean_percent * current_rule->euclidean_percent) <= euclidean_percent_squared) + continue; + //Rule applies + if(!match_target((struct map_target *)(current_rule + offsetof (struct map_target_separation_rule, target)), tx, ty, civ_id)) + continue; + //Increment rule counter + rule_matches[i] += 1; + //Check if rule max count is exceeded and exit early + if (current_rule->max_count < rule_matches[i]) { + free (rule_matches); + return CLV_CITY_TOO_CLOSE; //Kinda abusing this name since we now check for other things } } - } - return base_result; - - } else - return base_result; + } + //Check if each rule minimum count is met - no need to check max count, we do that when incrementing + for (int i = 0; i < is->current_config.count_minimum_city_separation_rules; i++) { + struct map_target_separation_rule * current_rule = city_separation_rules + i * sizeof(struct map_target_separation_rule); + if (!map_target_separation_rule_active(current_rule, civ_id)) + continue; + if (rule_matches[i] < current_rule->min_count) { + free (rule_matches); + return CLV_CITY_TOO_CLOSE; //Kinda abusing this name since we now check for other things + } + } + free (rule_matches); + return CLV_OK; } bool