diff --git a/C3X.h b/C3X.h index c130980a..5d5ffadd 100644 --- a/C3X.h +++ b/C3X.h @@ -84,6 +84,16 @@ struct work_area_improvement { int work_area_radius_bonus; }; +struct unit_visibility_rule { + int unit_id; + enum UnitTypeClasses unit_class; + + int base_visibility; + int terrain_bonus_multiplier; + int fortification_bonus; + bool fortification_bonus_continent_lock; +}; + enum retreat_rules { RR_STANDARD = 0, RR_NONE, @@ -483,6 +493,19 @@ struct c3x_config { int ai_auto_build_great_wall_strategy; bool enable_city_work_radii_highlights; + + bool enable_alternate_view_distance_logic; //enable the whole system or not + int base_visibility_range; //should default to 1 + int terrain_visibility_see_height[14]; //most tiles are 1, mountains+volcanoes are 2 + int terrain_visibility_seen_height[14]; //most tiles are 0, forest+jungle+hills are 1, mountains+volcanoes are 2 + int terrain_visibility_bonus[14]; //most tiles are 0, hills+mountains+volcanoes are 1 + bool terrain_visibility_bonus_can_stack; //whether seeing hills and being on a hill provides double the bonus + bool terrain_visibility_flat_bonus[14]; //water tiles provide height bonus to tiles being seen across them [ie +2] / adjacent tiles always seen + int terrain_visibility_flat_bonus_limit; //maximum size of the flat bonus, in tiles + bool terrain_visibility_flat_bonus_can_stack; //whether flat bonus and regular bonus can both apply at once + struct unit_visibility_rule * unit_visibility_rule_list; //should default to naval unit type has +1 range in fortification with continent lock + int c_unit_visibility_rules; + //tiles blocked by obstructions are visible if *either* intermediate tile is not blocking in height. }; enum stackable_command { diff --git a/civ_prog_objects.csv b/civ_prog_objects.csv index 53787e13..729f2e47 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -970,4 +970,10 @@ 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, 0x5BA010, 0x5C8AD0, 0x5BA090, "Unit_can_see_tile", "bool (__fastcall *) (Unit * this, int edx, int x, int y)" +define, 0x44A8D0, 0x44C870, 0x44A950, "Map_chebyshev_distance", "int (__fastcall *) (Map * this, int edx, int x1, int y1, int x2, int y2)" +define, 0x5BA1F9, 0x5C8CD6, 0x5BA279, "ADDR_UNIT_VISIBILITY_RADIUS_1", "byte *" +define, 0x5BA29A, 0x5C8D76, 0x5BA32A, "ADDR_UNIT_VISIBILITY_RADIUS_2", "byte *" +define, 0x5BA5F4, 0x5C90CD, 0x5BA674, "ADDR_UNIT_VISIBILITY_RADIUS_3", "byte *" +define, 0x5C811A, 0x5D6FF5, 0x5C819A, "ADDR_UNIT_TO_UNIT_VISIBILITY_RADIUS", "byte *" +define, 0x5BB7E6, 0x5CA333, 0x5BB866, "ADDR_CIV_UNIT_VISIBILITY_RADIUS", "byte *" diff --git a/default.c3x_config.ini b/default.c3x_config.ini index 6b2d90ee..5e561ed4 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -915,6 +915,32 @@ override_barbarian_activity_level_for_scenario_maps = none ; types at the start of the game so they behave like normal MGLs spawned during a game. initialize_preplaced_scenario_leaders_as_mgls = false +[==================] +[=== VISIBILITY ===] +[==================] + +; Whether to enable the alternate visibility system. Within this system, the total view distance cannot exceed 7 tiles due to technical limitations. Any visibility beyond this will be clamped. +enable_alternate_view_distance_logic = false + +; The base view distance of units +base_visibility_range = 1 +; The following settings have an entry for each terrain type, annotated and abbreviated as follows +; [DESRT, PLAIN, GRSLD, TNDRA, FLDPL, HILLS, MNTNS, FORST, JUNGL, MARSH, VLCNO, COAST, SEA, OCEAN] +; The height a unit is considered to be standing at when on this type of tile. +terrain_visibility_see_height = [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1] +; The hight a tile is considered to be seen from another tile. If this equals or exceed the see height, it will occlude tiles beyond. +terrain_visibility_seen_height = [0, 0, 0, 0, 0, 1, 2, 1, 1, 0, 2, 0, 0, 0] +; The bonus sight range granted by standing on or looking at a given tile +terrain_visibility_bonus = [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0] +; Whether a unit on a tile with a bonus and seeing a tile with bonus is double rewarded. Otherwise, the large value is used. This may be necessary if you wish to use negative bonuses. +terrain_visibility_bonus_can_stack = false +; Whether tiles of the given type provide bonus view distance for seeing across them. This config only supports 0 and 1. +terrain_visibility_flat_bonus = [false, false, false, false, false, false, false, false, false, false, false, true, true, true] +; The number of times the flat bonus may be applied. +terrain_visibility_flat_bonus_limit = 1 +; Whether the flat bonus may stack with the regular, height-based bonus. +terrain_visibility_flat_bonus_can_stack = false + [==================] [=== AESTHETICS ===] [==================] diff --git a/injected_code.c b/injected_code.c index 4b42527f..69aa982c 100644 --- a/injected_code.c +++ b/injected_code.c @@ -301,6 +301,21 @@ pop_up_in_game_error (char const * msg) patch_show_popup (popup, __, 0, 0); } +void error_message2(char* cstr, int num1, int num2, int num3) +{ + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "DEBUG_ERROR", -1, 0, 0, 0); + char s[200]; + snprintf (s, sizeof s, cstr, num1, num2, num3); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, false); + patch_show_popup (popup, __, 0, 0); +} +void error_message(char* cstr) +{ + error_message2(cstr, 0, 0, 0); +} + void memoize (int val) { @@ -1496,6 +1511,49 @@ read_recognizables (struct string_slice const * s, return success ? -1 : cursor - extracted_slice; } +int +read_fixed_int_array (struct string_slice const * s, int * array, int count) +{ + char * cur = s->str; + int ival = 0; + if(!skip_white_space (&cur)) + return 0; + for(int i=0; istr + s->len) + return 0; + return 1; +} + +int +read_fixed_bool_array (struct string_slice const * s, bool * array, int count) +{ + char * cur = s->str; + int ival = 0; + if(!skip_white_space (&cur)) + return 0; + for(int i=0; istr + s->len) + return 0; + return 1; +} + // Reads a "sidtable" from text. A sidtable maps strings to IDs of scenario objects. The string keys are also expected to be a type of scenario // object. This method reads text such as: // Factory: Battleship, Stable: Horseman Knight Cavalry, "Siege Workshop": Catapult Trebuchet @@ -2642,10 +2700,28 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) else handle_config_error (&p, CPE_BAD_BOOL_VALUE); + } else if (slice_matches_str (&p.key, "terrain_visibility_see_height")) { + if (read_fixed_int_array(&value, cfg->terrain_visibility_see_height, 14) == 0) { + handle_config_error (&p, CPE_GENERIC); + } + } else if (slice_matches_str (&p.key, "terrain_visibility_seen_height")) { + if (read_fixed_int_array(&value, cfg->terrain_visibility_seen_height, 14) == 0) { + handle_config_error (&p, CPE_GENERIC); + } + } else if (slice_matches_str (&p.key, "terrain_visibility_bonus")) { + if (read_fixed_int_array(&value, cfg->terrain_visibility_bonus, 14) == 0) { + handle_config_error (&p, CPE_GENERIC); + } + } else if (slice_matches_str (&p.key, "terrain_visibility_flat_bonus")) { + if (read_fixed_bool_array(&value, cfg->terrain_visibility_flat_bonus, 14) == 0) { + handle_config_error (&p, CPE_GENERIC); + } + } else { handle_config_error (&p, CPE_BAD_KEY); } + cfg->c_unit_visibility_rules = 0; } else { // Failed to parse value handle_config_error (&p, CPE_BAD_VALUE); @@ -16785,6 +16861,26 @@ apply_machine_code_edits (struct c3x_config const * cfg, bool at_program_start) // replacing 0x75 (= jnz) with 0xEB (= uncond. jump) WITH_MEM_PROTECTION (ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS, 1, PAGE_EXECUTE_READWRITE) *ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS = is->current_config.expand_civilopedia_unit_stats ? 0xEB : 0x75; + + int max_sight_range = calc_max_visibility_range(); + int max_tile_iter = (2*max_sight_range+1)*(2*max_sight_range+1); + int max_tile_iter_2 = (2*max_sight_range-1)*(2*max_sight_range-1); + // Modify iterator limits for updating unit visibility + WITH_MEM_PROTECTION (ADDR_UNIT_VISIBILITY_RADIUS_1, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_UNIT_VISIBILITY_RADIUS_1 = (byte)(max_tile_iter); + } + WITH_MEM_PROTECTION (ADDR_UNIT_VISIBILITY_RADIUS_2, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_UNIT_VISIBILITY_RADIUS_2 = (byte)(max_tile_iter); + } + WITH_MEM_PROTECTION (ADDR_UNIT_VISIBILITY_RADIUS_3, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_UNIT_VISIBILITY_RADIUS_3 = (byte)(max_tile_iter); + } + WITH_MEM_PROTECTION (ADDR_UNIT_TO_UNIT_VISIBILITY_RADIUS, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_UNIT_TO_UNIT_VISIBILITY_RADIUS = (byte)(max_tile_iter); + } + WITH_MEM_PROTECTION (ADDR_CIV_UNIT_VISIBILITY_RADIUS, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_CIV_UNIT_VISIBILITY_RADIUS = (byte)(max_tile_iter_2); + } } void @@ -17817,6 +17913,9 @@ patch_init_floating_point () {"allow_corruption_in_capital" , false, offsetof (struct c3x_config, allow_corruption_in_capital)}, {"allow_sale_of_small_wonders" , false, offsetof (struct c3x_config, allow_sale_of_small_wonders)}, {"initialize_preplaced_scenario_leaders_as_mgls" , false, offsetof (struct c3x_config, initialize_preplaced_scenario_leaders_as_mgls)}, + {"enable_alternate_view_distance_logic" , false, offsetof (struct c3x_config, enable_alternate_view_distance_logic)}, + {"terrain_visibility_bonus_can_stack" , false, offsetof (struct c3x_config, terrain_visibility_bonus_can_stack)}, + {"terrain_visibility_flat_bonus_can_stack" , false, offsetof (struct c3x_config, terrain_visibility_flat_bonus_can_stack)}, }; struct integer_config_option { @@ -17866,6 +17965,8 @@ patch_init_floating_point () {"ai_bridge_eval_lake_tile_threshold" , 6, offsetof (struct c3x_config, ai_bridge_eval_lake_tile_threshold)}, {"ai_city_district_max_build_wait_turns" , 20, offsetof (struct c3x_config, ai_city_district_max_build_wait_turns)}, {"per_extraterritorial_colony_relation_penalty" , 0, offsetof (struct c3x_config, per_extraterritorial_colony_relation_penalty)}, + {"base_visibility_range" , 1, offsetof (struct c3x_config, base_visibility_range)}, + {"terrain_visibility_flat_bonus_limit" , 1, offsetof (struct c3x_config, terrain_visibility_flat_bonus_limit)}, }; is->kernel32 = (*p_GetModuleHandleA) ("kernel32.dll"); @@ -21776,6 +21877,307 @@ patch_Unit_can_load (Unit * this, int edx, Unit * passenger) return tr; } +bool check_tile_heights_less_than(int tx1, int ty1, int tx2, int ty2, int height) +{ + Tile * inter_tile = tile_at(tx1, ty1); + enum SquareTypes inter_type = inter_tile->vtable->m50_Get_Square_BaseType (inter_tile); + if (inter_type < 0 || inter_type >= 14) { + error_message("invalid type 1"); + return false; //Unhandled!!! + } + int inter_height = is->current_config.terrain_visibility_seen_height[inter_type]; + if (inter_height < height) { + return true; + } + if (tx1 != tx2 || ty1 != ty2) { + inter_tile = tile_at(tx2, ty2); + inter_type = inter_tile->vtable->m50_Get_Square_BaseType (inter_tile); + if (inter_type < 0 || inter_type >= 14) { + error_message("invalid type 2"); + return false; //Unhandled!!! + } + + inter_height = is->current_config.terrain_visibility_seen_height[inter_type]; + if (inter_height < height) { + return true; + } + } + return false; +} +void add_tile_flat_bonus(int * flat_bonus, int tx, int dx, int tx1, int ty1, int tx2, int ty2) +{ + if (*flat_bonus >= is->current_config.terrain_visibility_flat_bonus_limit) { + return; + } + int total_diff = int_abs(dx) - int_abs(tx); + if (total_diff > is->current_config.terrain_visibility_flat_bonus_limit) { + return; + } + Tile * inter_tile = tile_at(tx1, ty1); + enum SquareTypes inter_type = inter_tile->vtable->m50_Get_Square_BaseType (inter_tile); + if (inter_type < 0 || inter_type >= 14) { + error_message("invalid type 1"); + return; //Unhandled!!! + } + if (is->current_config.terrain_visibility_flat_bonus[inter_type]) { + *flat_bonus += 1; + return; + } + if (tx1 != tx2 || ty1 != ty2) { + inter_tile = tile_at(tx2, ty2); + inter_type = inter_tile->vtable->m50_Get_Square_BaseType (inter_tile); + if (inter_type < 0 || inter_type >= 14) { + error_message("invalid type 2"); + return; //Unhandled!!! + } + if (is->current_config.terrain_visibility_flat_bonus[inter_type]) { + *flat_bonus += 1; + return; + } + } + return; +} + +void map_abstract_coords_to_tile_coords(int * dx, int * dy, int reference_x, int reference_y) +{ + int tx = *dx-*dy + reference_x; + int ty = *dy+*dx + reference_y; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); + *dx = tx; + *dy = ty; + /**dx = *dx + reference_x; + *dy = *dy + reference_y;*/ +} +void map_tile_coords_to_abstract_coords(int * dx, int * dy, int reference_x, int reference_y) +{ + int diffx = *dx-reference_x; + int diffy = *dy-reference_y; + Map * map = &p_bic_data->Map; + if (map->Flags & 1) { + if (diffx < -(map->Width/2)) { + diffx += map->Width; + } else if (diffx > (map->Width/2)) { + diffx -= map->Width; + } + } + if (map->Flags & 2) { + if (diffy < -(map->Height/2)) { + diffy += map->Height; + } else if (diffy > (map->Height/2)) { + diffy -= map->Height; + } + } + int ax = (diffx + diffy)/2; + int ay = (diffy - diffx)/2; + //maybe minimise somehow + *dx = ax; + *dy = ay; + /**dx = *dx - reference_x; + *dy = *dy - reference_y;*/ +} + +int calc_max_visibility_range () +{ + if (!is->current_config.enable_alternate_view_distance_logic) + return 3; + //Technically this calculates vanilla at 4 rather than 3 as expected in the binary. + //This is because a boat on a mountain would theoretically achieve this. + int max_bonus = 0; + for (int i=0; i<14; i++) { + int bonus = is->current_config.terrain_visibility_bonus[i]; + if (bonus > max_bonus) max_bonus = bonus; + } + if (is->current_config.terrain_visibility_bonus_can_stack) + max_bonus *= 2; + + int max = is->current_config.base_visibility_range + max_bonus; + for (int i=0; icurrent_config.c_unit_visibility_rules; i++) { + struct unit_visibility_rule rule = is->current_config.unit_visibility_rule_list[i]; + int unitsight = rule.base_visibility + max_bonus * rule.terrain_bonus_multiplier + rule.fortification_bonus; + + if (is->current_config.terrain_visibility_flat_bonus_can_stack) { + unitsight += is->current_config.terrain_visibility_flat_bonus_limit; + } else { + int altmax = rule.base_visibility + is->current_config.terrain_visibility_flat_bonus_limit + rule.fortification_bonus; + if (altmax > unitsight) + unitsight = altmax; + } + if (unitsight > max) max = unitsight; + } + + if (max < 0) max = 0; + if (max > 7) max = 7; + //Limited to 7. 7*2+1 = 15; 15*15 = 225; largest value in one byte. + return max; +} + +bool __fastcall +deferred_Unit_can_see_tile (Unit * this, int edx, int x, int y) +{ + if (!is->current_config.enable_alternate_view_distance_logic) + return Unit_can_see_tile(this, edx, x, y); + int dist = Map_chebyshev_distance(&p_bic_data->Map, edx, this->Body.X, this->Body.Y, x, y); + if (dist >= 8) return false; + if (dist == 0) return true; + + struct unit_visibility_rule current_rules; + current_rules.base_visibility = is->current_config.base_visibility_range; + current_rules.terrain_bonus_multiplier = 1; + current_rules.fortification_bonus = 0; + current_rules.fortification_bonus_continent_lock = false; + + for (int i=0; icurrent_config.c_unit_visibility_rules; i++) { + struct unit_visibility_rule rule = is->current_config.unit_visibility_rule_list[i]; + if (rule.unit_id > 0) { + if (rule.unit_id == this->Body.UnitTypeID) { + current_rules = rule; + break; + } + } else { + UnitType const * unit_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + if (rule.unit_class == unit_type->Unit_Class) { + current_rules = rule; + break; + } + } + } + //temp hack + UnitType const * unit_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + if (unit_type->Unit_Class == 1) { + current_rules.fortification_bonus = 2; + current_rules.fortification_bonus_continent_lock = true; + } + + Tile * local_tile = tile_at(this->Body.X, this->Body.Y); + Tile * seen_tile = tile_at(x, y); + + int fortification_bonus; + if (this->Body.UnitState == 1) { + fortification_bonus = current_rules.fortification_bonus; + } else { + fortification_bonus = 0; + } + enum SquareTypes local_type = local_tile->vtable->m50_Get_Square_BaseType (local_tile); + if (local_type < 0 || local_type >= 14) { + error_message("invalid type 3"); + return false; //Unhandled!!! + } + enum SquareTypes seen_type = seen_tile->vtable->m50_Get_Square_BaseType (seen_tile); + if (seen_type < 0 || seen_type >= 14) { + error_message("invalid type 3"); + return false; //Unhandled!!! + } + int height_local = is->current_config.terrain_visibility_see_height[local_type]; + int height_seen = is->current_config.terrain_visibility_see_height[seen_type]; + int height_for_occlusion = height_local; + if (height_local >= height_seen) { + height_for_occlusion = height_local; + } else { + height_for_occlusion = height_seen; + } + + int bonus_local = is->current_config.terrain_visibility_bonus[local_type]; + int bonus_seen = is->current_config.terrain_visibility_bonus[seen_type]; + int bonus_range; + if (is->current_config.terrain_visibility_bonus_can_stack) { + bonus_range = (bonus_seen+bonus_local) * current_rules.terrain_bonus_multiplier; + } else if (bonus_local >= bonus_seen) { + bonus_range = bonus_local * current_rules.terrain_bonus_multiplier; + } else { + bonus_range = bonus_seen * current_rules.terrain_bonus_multiplier; + } + int sightDistance = current_rules.base_visibility + bonus_range + fortification_bonus; + + if (dist > sightDistance + is->current_config.terrain_visibility_flat_bonus_limit) { + return false; + } + + //Include radar!! + if (current_rules.fortification_bonus_continent_lock && fortification_bonus > 0) { + if (local_tile->vtable->m46_Get_ContinentID(local_tile) == seen_tile->vtable->m46_Get_ContinentID(seen_tile)) { + if (dist <= sightDistance && seen_type > 10)//why am i hacking this so bad + return true; + } + //temp hack + sightDistance -= 2; + } + + int flat_bonus = 0; + + int dx = x; + int dy = y; + map_tile_coords_to_abstract_coords(&dx, &dy, this->Body.X, this->Body.Y); + int absdx = int_abs(dx); + int absdy = int_abs(dy); + if (absdx >= absdy && dx != 0) { + int tx = 0; + while (tx != dx) { + if (tx < dx) tx++; else tx--; + if (tx == dx) break; + int ty1; + int ty2; + if (dx * dy >= 0) { + ty1 = (2 * tx * dy + absdx)/(2 * dx); + ty2 = (2 * tx * dy + absdx - 1)/(2 * dx); + } else { + ty1 = -(-2 * tx * dy + absdx)/(2 * dx); + ty2 = -(-2 * tx * dy + absdx - 1)/(2 * dx); + } + int tx1 = tx; + int tx2 = tx; + map_abstract_coords_to_tile_coords(&tx1, &ty1, this->Body.X, this->Body.Y); + map_abstract_coords_to_tile_coords(&tx2, &ty2, this->Body.X, this->Body.Y); + if (!check_tile_heights_less_than(tx1, ty1, tx2, ty2, height_for_occlusion)) { + return false; + } + add_tile_flat_bonus(&flat_bonus, tx, dx, tx1, ty1, tx2, ty2); + } + } else if (absdy >= absdx && dy != 0) { + int ty = 0; + while (ty != dy) { + if (ty < dy) ty++; else ty--; + if (ty == dy) break; + int tx1; + int tx2; + if (dx * dy >= 0) { + tx1 = (2 * ty * dx + absdy)/(2 * dy); + tx2 = (2 * ty * dx + absdy - 1)/(2 * dy); + } else { + tx1 = -(-2 * ty * dx + absdy)/(2 * dy); + tx2 = -(-2 * ty * dx + absdy - 1)/(2 * dy); + } + int ty1 = ty; + int ty2 = ty; + map_abstract_coords_to_tile_coords(&tx1, &ty1, this->Body.X, this->Body.Y); + map_abstract_coords_to_tile_coords(&tx2, &ty2, this->Body.X, this->Body.Y); + if (!check_tile_heights_less_than(tx1, ty1, tx2, ty2, height_for_occlusion)) { + return false; + } + add_tile_flat_bonus(&flat_bonus, ty, dy, tx1, ty1, tx2, ty2); + //we could exit check each tile but it's a bit much work + } + } + if (is->current_config.terrain_visibility_flat_bonus_can_stack) { + sightDistance += flat_bonus; + } else { + int flatsight = current_rules.base_visibility + fortification_bonus + flat_bonus; + if (flatsight > sightDistance) + sightDistance = flatsight; + } + return dist <= sightDistance; +} + +bool __fastcall +patch_Unit_can_see_tile (Unit * this, int edx, int x, int y) +{ + //bool vanilla = Unit_can_see_tile(this, edx, x, y); + bool modded = deferred_Unit_can_see_tile(this, edx, x, y); + //if (vanilla != modded) { + // error_message2("behaviour mismatch! %d %d %d", x, y, vanilla?1:0); + //} + return modded; +} + void __fastcall patch_Unit_load (Unit * this, int edx, Unit * transport) {