diff --git a/dataobj/settings.cc b/dataobj/settings.cc index e9aa2afd45..2ed428208b 100644 --- a/dataobj/settings.cc +++ b/dataobj/settings.cc @@ -343,6 +343,11 @@ settings_t::settings_t() : allow_higher_flight = true; use_route_cache = false; + + transit_by_foot = false; + foot_path_weight = 24; + foot_path_time_ticks = 1800; + walk_cost_to_halt = false; } @@ -1055,6 +1060,20 @@ void settings_t::rdwr(loadsave_t *file) signal_reverse_front_back = false; roadsign_reverse_front_back = false; } + if( file->get_OTRP_version() > 55 ) { + file->rdwr_bool(transit_by_foot); + file->rdwr_long(foot_path_weight); + file->rdwr_long(foot_path_time_ticks); + } else { + transit_by_foot = false; + foot_path_weight = 24; + foot_path_time_ticks = 1800; + } + if( file->get_OTRP_version() > 56 ) { + file->rdwr_bool(walk_cost_to_halt); + } else { + walk_cost_to_halt = false; + } if( file->is_version_atleast(122, 1) ) { file->rdwr_enum(climate_generator); file->rdwr_byte( wind_direction ); @@ -1888,6 +1907,11 @@ void settings_t::parse_simuconf( tabfile_t& simuconf, sint16& disp_width, sint16 base_waiting_ticks_for_ship_convoi = contents.get_int("base_waiting_ticks_for_ship_convoi", base_waiting_ticks_for_ship_convoi); base_waiting_ticks_for_air_convoi = contents.get_int("base_waiting_ticks_for_air_convoi", base_waiting_ticks_for_air_convoi); + transit_by_foot = contents.get_int("transit_by_foot", transit_by_foot) != 0; + foot_path_weight = contents.get_int("foot_path_weight", foot_path_weight); + foot_path_time_ticks = contents.get_int("foot_path_time_ticks", foot_path_time_ticks); + walk_cost_to_halt = contents.get_int("walk_cost_to_halt", walk_cost_to_halt) != 0; + // Default pak file path objfilename = ltrim(contents.get_string("pak_file_path", objfilename.c_str() ) ); diff --git a/dataobj/settings.h b/dataobj/settings.h index 2b34b6810d..8c1e0915b5 100644 --- a/dataobj/settings.h +++ b/dataobj/settings.h @@ -384,6 +384,15 @@ class settings_t // can unload cargo even if stop length is too short bool allow_unload_longer_convoy; + // transit by foot between overlapping pax stops + bool transit_by_foot; + uint32 foot_path_weight; // added to route cost for walking connection + uint32 foot_path_time_ticks; // added to journey time for time-based routing + // When true, the walking distance from a passenger's origin/destination tile to each + // candidate halt is included in the route cost so that nearer halts are preferred. + // When false, all halts within station coverage are treated as equidistant (old behaviour). + bool walk_cost_to_halt; + public: /* the big cost section */ sint32 maint_building; // normal building @@ -772,6 +781,15 @@ class settings_t bool is_allow_unload_longer_convoy() const { return allow_unload_longer_convoy; } bool is_using_route_cache() const { return use_route_cache; } + + bool is_transit_by_foot() const { return transit_by_foot; } + void set_transit_by_foot(bool v) { transit_by_foot = v; } + uint32 get_foot_path_weight() const { return foot_path_weight; } + void set_foot_path_weight(uint32 v) { foot_path_weight = v; } + uint32 get_foot_path_time_ticks() const { return foot_path_time_ticks; } + void set_foot_path_time_ticks(uint32 v) { foot_path_time_ticks = v; } + bool is_walk_cost_to_halt() const { return walk_cost_to_halt; } + void set_walk_cost_to_halt(bool v) { walk_cost_to_halt = v; } }; #endif diff --git a/gui/halt_info.cc b/gui/halt_info.cc index 2f49183f75..128f2049ee 100644 --- a/gui/halt_info.cc +++ b/gui/halt_info.cc @@ -794,11 +794,18 @@ void gui_halt_detail_t::update_connections( halthandle_t h ) for (uint i=0; i const& connections = halt->get_connections(i); if( !connections.empty() ) { + // Only show vehicle connections here; foot-paths are listed separately below. + vector_tpl sorted; + FOR(vector_tpl, const& conn, connections) { + if( conn.halt.is_bound() && !conn.is_foot_path ) { + sorted.insert_unique_ordered(conn, gui_halt_detail_t::compare_connection); + } + } + if( sorted.empty() ) { continue; } gui_label_buf_t *lb = new_component_span(2); lb->buf().append(" \xC2\xB7"); const goods_desc_t* info = goods_manager_t::get_info_catg_index(i); - // If it is a special freight, we display the name of the good, otherwise the name of the category. lb->buf().append(translator::translate(info->get_catg()==0 ? info->get_name() : info->get_catg_name() ) ); #if MSG_LEVEL>=4 if( halt->is_transfer(i) ) { @@ -808,15 +815,8 @@ void gui_halt_detail_t::update_connections( halthandle_t h ) lb->buf().append(":\n"); lb->update(); - vector_tpl sorted; - FOR(vector_tpl, const& conn, connections) { - if( conn.halt.is_bound() ) { - sorted.insert_unique_ordered(conn, gui_halt_detail_t::compare_connection); - } - } const bool is_tgbr_enabled = world()->get_settings().get_time_based_routing_enabled(i); FOR(vector_tpl, const& conn, sorted) { - has_stops = true; button_t *pb = new_component(); @@ -825,10 +825,9 @@ void gui_halt_detail_t::update_connections( halthandle_t h ) gui_label_buf_t *lb = new_component(); if( is_tgbr_enabled ) { - // Show the estimated journey time in the divided time units - const uint16 weight = world()->tick_to_divided_time(conn.weight); + const uint16 weight = world()->tick_to_divided_time(conn.weight); std::visit([&](const auto& t) { - lb->buf().printf("%s <%u> - %s", conn.halt->get_name(), weight, t.is_bound() ? t->get_name() : "Unavailable"); + lb->buf().printf("%s <%u> - %s", conn.halt->get_name(), weight, t.is_bound() ? t->get_name() : "?"); }, conn.best_weight_traveler); } else { lb->buf().printf("%s <%u>", conn.halt->get_name(), conn.weight); @@ -842,6 +841,42 @@ void gui_halt_detail_t::update_connections( halthandle_t h ) insert_show_nothing(); } + // Foot-path connections (transit by foot) — only shown when feature is enabled + if( world()->get_settings().is_transit_by_foot() ) { + vector_tpl const& pax_conns = halt->get_connections(goods_manager_t::INDEX_PAS); + vector_tpl foot_sorted; + FOR(vector_tpl, const& conn, pax_conns) { + if( conn.halt.is_bound() && conn.is_foot_path ) { + foot_sorted.insert_unique_ordered(conn, gui_halt_detail_t::compare_connection); + } + } + insert_empty_row(); + new_component_span("Reachable on foot", 2); + if( !foot_sorted.empty() ) { + const bool is_tgbr_enabled = world()->get_settings().get_time_based_routing_enabled(goods_manager_t::INDEX_PAS); + const uint32 base_rc = world()->get_settings().get_foot_path_weight(); + const uint32 base_jt = world()->get_settings().get_foot_path_time_ticks(); + FOR(vector_tpl, const& conn, foot_sorted) { + button_t *pb = new_component(); + pb->init( button_t::posbutton_automatic, NULL); + pb->set_targetpos3d( conn.halt->get_basis_pos3d() ); + + gui_label_buf_t *lb = new_component(); + // Reverse-calculate tile distance from weight (weight = base * dist). + const uint32 base = is_tgbr_enabled ? base_jt : base_rc; + const uint32 dist = (base > 0) ? (conn.weight / base) : 0; + if( is_tgbr_enabled ) { + lb->buf().printf("%s (%u tiles) <%u>", conn.halt->get_name(), dist, world()->tick_to_divided_time(conn.weight)); + } else { + lb->buf().printf("%s (%u tiles) <%u>", conn.halt->get_name(), dist, conn.weight); + } + lb->update(); + } + } else { + insert_show_nothing(); + } + } + // ok, we have now this counter for pending updates destination_counter = halt->get_reconnect_counter(); last_connection_update_counter = halt->get_connection_update_counter(); diff --git a/gui/route_search_frame.cc b/gui/route_search_frame.cc index 49d894239b..ced7f49e96 100644 --- a/gui/route_search_frame.cc +++ b/gui/route_search_frame.cc @@ -1,6 +1,7 @@ #include "route_search_frame.h" #include "../dataobj/schedule.h" #include "../dataobj/translator.h" +#include "../simplan.h" #include "components/gui_divider.h" #include "components/gui_image.h" #include "minimap.h" @@ -11,6 +12,7 @@ #include "../simware.h" #include "../simworld.h" #include +#include class gui_traveler_button_t : public button_t, public action_listener_t { @@ -58,13 +60,18 @@ class gui_halt_button_t : public button_t, public action_listener_t { route_search_frame_t::route_search_frame_t() : gui_frame_t( translator::translate("Pax route search") ), -from_halt_label("From:"), -dest_halt_label("To:"), -result_container(1, 0) +from_halt_label("From halt:"), +dest_halt_label("To halt:"), +from_koord_label("From (x,y):"), +dest_koord_label("To (x,y):"), +result_container(1, 0), +from_koord(koord::invalid), +dest_koord(koord::invalid) { - // clear the buffer snprintf(from_halt_input_text, lengthof(from_halt_input_text), ""); snprintf(dest_halt_input_text, lengthof(dest_halt_input_text), ""); + snprintf(from_koord_text, lengthof(from_koord_text), ""); + snprintf(dest_koord_text, lengthof(dest_koord_text), ""); set_table_layout(1,0); { @@ -84,7 +91,6 @@ result_container(1, 0) for( int i=0; i < goods_manager_t::get_count(); i++ ) { const goods_desc_t *ware = goods_manager_t::get_info(i); if( ware->get_catg() == 0 && ware->get_index() > 2 ) { - // Special freight: Each good is special viewable_freight_types.append(ware); freight_type_c.new_component( translator::translate(ware->get_name()), SYSCOL_TEXT) ; } @@ -95,7 +101,8 @@ result_container(1, 0) freight_type_c.set_focusable( true ); freight_type_c.add_listener( this ); - add_table(5, 1); + // Row 1: halt name inputs + add_table(4, 1); { from_halt_input.set_text(from_halt_input_text, lengthof(from_halt_input_text)); dest_halt_input.set_text(dest_halt_input_text, lengthof(dest_halt_input_text)); @@ -106,6 +113,18 @@ result_container(1, 0) } end_table(); + // Row 2: coordinate inputs (takes precedence over halt name when non-empty) + add_table(4, 1); + { + from_koord_input.set_text(from_koord_text, lengthof(from_koord_text)); + dest_koord_input.set_text(dest_koord_text, lengthof(dest_koord_text)); + add_component(&from_koord_label); + add_component(&from_koord_input); + add_component(&dest_koord_label); + add_component(&dest_koord_input); + } + end_table(); + add_table(3, 2); { search_button.init(button_t::roundbox, "Search"); @@ -118,7 +137,6 @@ result_container(1, 0) add_component(&freight_type_c); - bt_show_non_traveled.init(button_t::square_state, "Show Non-Traveled Section"); bt_show_non_traveled.pressed = true; bt_show_non_traveled.add_listener(this); @@ -130,7 +148,7 @@ result_container(1, 0) result_container.set_table_layout(1,0); add_component(&result_container); - result_container.new_component("Please enter halt names and press search."); + result_container.new_component("Enter halt names or tile coordinates (x,y) and press Search."); set_resizemode(diagonal_resize); reset_min_windowsize(); @@ -200,10 +218,19 @@ void route_search_frame_t::append_connection_row(haltestelle_t::connection_t con auto label_with_buf = result_container.new_component(); label_with_buf->buf().append(text); - linehandle_t result_line = std::holds_alternative(connection.best_weight_traveler) ? + if( connection.is_foot_path ) { + // foot-path connection: no vehicle or line, just show walk indicator + result_container.new_component(); + result_container.new_component(); + result_container.new_component("(walk)"); + result_container.end_table(); + return; + } + + linehandle_t result_line = std::holds_alternative(connection.best_weight_traveler) ? std::get(connection.best_weight_traveler) : linehandle_t(); result_container.new_component(result_line); - convoihandle_t cnv = result_line.is_bound()?( result_line->count_convoys()>0 ? result_line->get_convoy(0) : convoihandle_t() ) : std::get(connection.best_weight_traveler); + convoihandle_t cnv = result_line.is_bound()?( result_line->count_convoys()>0 ? result_line->get_convoy(0) : convoihandle_t() ) : (std::holds_alternative(connection.best_weight_traveler) ? std::get(connection.best_weight_traveler) : convoihandle_t()); if ( cnv.is_bound() ) { auto original_sched = cnv->get_schedule(); @@ -256,6 +283,7 @@ void route_search_frame_t::append_connection_row(haltestelle_t::connection_t con if (i == start_idx) break; } } else { + result_container.end_table(); return; } @@ -318,45 +346,200 @@ void route_search_frame_t::append_halt_row(halthandle_t halt) { result_container.end_table(); } +void route_search_frame_t::append_pos_row(koord pos) { + result_container.add_table(2, 1); + result_container.new_component(); // alignment placeholder (halt_row has a button here) + char buf[32]; + snprintf(buf, sizeof(buf), "(%d, %d)", pos.x, pos.y); + auto *lbl = result_container.new_component(); + lbl->buf().append(buf); + result_container.end_table(); +} + +// static +koord route_search_frame_t::parse_koord(const char* text) +{ + if(!text || text[0] == '\0') return koord::invalid; + int x = -1, y = -1; + if(sscanf(text, "%d,%d", &x, &y) == 2 && x >= 0 && y >= 0) { + return koord(x, y); + } + return koord::invalid; +} + void route_search_frame_t::search_route() { // reset selection minimap_t::get_instance()->set_selected_cnv( convoihandle_t(), true ); minimap_t::get_instance()->set_selected_route( nullptr, nullptr, true ); result_container.remove_all(); - from_halt = find_halt(from_halt_input.get_text()); - if( !from_halt.is_bound() ) { - result_container.new_component("From halt not found."); - return; + + // Parse coordinate inputs; they take precedence over halt-name inputs when non-empty. + from_koord = parse_koord(from_koord_input.get_text()); + dest_koord = parse_koord(dest_koord_input.get_text()); + + // Helper: collect all enabled halts covering a tile position. + auto collect_halts = [&](koord pos, vector_tpl& out) { + const planquadrat_t *plan = world()->access(pos); + if(plan) { + for(uint h = 0; h < plan->get_haltlist_count(); h++) { + halthandle_t h2 = plan->get_haltlist()[h]; + if(h2.is_bound() && h2->is_enabled(search_ware_index)) { + out.append_unique(h2); + } + } + } + }; + + // Resolve from-position and collect all candidate start halts. + static vector_tpl start_halts(16); + start_halts.clear(); + koord from_pos; + if((from_koord != koord::invalid)) { + from_pos = from_koord; + collect_halts(from_pos, start_halts); + if(start_halts.empty()) { + result_container.new_component("No halt found at From coordinate."); + return; + } } - dest_halt = find_halt(dest_halt_input.get_text()); - if( !dest_halt.is_bound() ) { - result_container.new_component("To halt not found."); - return; + else { + from_halt = find_halt(from_halt_input.get_text()); + if(!from_halt.is_bound()) { + result_container.new_component("From halt not found."); + return; + } + from_pos = from_halt->get_init_pos(); + start_halts.append(from_halt); } - minimap_t::get_instance()->set_from_dest_halt(from_halt, dest_halt); + + // Resolve destination position. + // When dest_koord is given: leave dummy_ware.ziel unbound so search_route evaluates ALL halts + // near dest_koord and picks the one with the lowest total route cost (transit + walking). + // When halt-name is given: fix the destination as before (backward-compatible). + koord dest_pos_for_search; + bool fix_dest = false; + if((dest_koord != koord::invalid)) { + dest_pos_for_search = dest_koord; + // Validate that at least one enabled halt exists near dest_koord. + static vector_tpl dest_halts_tmp(8); + dest_halts_tmp.clear(); + collect_halts(dest_koord, dest_halts_tmp); + if(dest_halts_tmp.empty()) { + result_container.new_component("No halt found at To coordinate."); + return; + } + dest_halt = dest_halts_tmp[0]; // preliminary; updated after search + } + else { + dest_halt = find_halt(dest_halt_input.get_text()); + if(!dest_halt.is_bound()) { + result_container.new_component("To halt not found."); + return; + } + dest_pos_for_search = dest_halt->get_init_pos(); + fix_dest = true; + } + ware_t dummy_ware = ware_t(); dummy_ware.menge = 0; dummy_ware.index = search_ware_index; - dummy_ware.set_zielpos(dest_halt->get_init_pos()); - dummy_ware.set_ziel(dest_halt); - halthandle_t start_halt_array[1] = {from_halt}; - haltestelle_t::search_route(start_halt_array, 1, false, dummy_ware); + dummy_ware.set_zielpos(dest_pos_for_search); + if(fix_dest) { + // Halt-name mode: pin the destination so only this halt is considered. + dummy_ware.set_ziel(dest_halt); + } + // Koord mode: ziel stays unbound — search_route discovers the best destination halt. + + // Use return_ware so we can learn the actual start halt chosen by the routing algorithm. + // When multiple start halts exist (koord mode), routing picks the one with the lowest + // total cost (walking to halt + transit), not simply the nearest one. + ware_t return_ware; + haltestelle_t::search_route(start_halts.begin(), (uint16)start_halts.get_count(), false, dummy_ware, + &return_ware, from_pos); + + // Update from_halt / dest_halt to what routing actually chose. + if(return_ware.get_ziel().is_bound()) { + from_halt = return_ware.get_ziel(); + } + if(!fix_dest && dummy_ware.get_ziel().is_bound()) { + dest_halt = dummy_ware.get_ziel(); + } + + minimap_t::get_instance()->set_from_dest_halt(from_halt, dest_halt); if( !dummy_ware.get_ziel().is_bound() ) { result_container.new_component("No route found!"); return; } + const uint8 ware_catg_idx = dummy_ware.get_desc()->get_catg_index(); + const bool tbgr = world()->get_settings().get_time_based_routing_enabled(ware_catg_idx); + const bool add_walk = world()->get_settings().is_transit_by_foot() + && world()->get_settings().is_walk_cost_to_halt(); + const uint32 walk_factor = tbgr ? world()->get_settings().get_foot_path_time_ticks() + : world()->get_settings().get_foot_path_weight(); + + // Pre-compute walking costs so they can be both displayed and counted in the total. + const uint32 origin_walk_raw = (add_walk && (from_koord != koord::invalid)) + ? koord_distance(from_pos, from_halt->get_init_pos()) * walk_factor : 0u; + const uint32 dest_walk_raw = (add_walk && (dest_koord != koord::invalid)) + ? koord_distance(dest_halt->get_init_pos(), dest_pos_for_search) * walk_factor : 0u; + + // Helper: emit a walking-leg connection row using the existing foot-path display code. + // append_connection_row handles is_foot_path=true without needing a real convoy/line. + auto append_walk_leg = [&](uint32 raw_weight) { + haltestelle_t::connection_t wc; + wc.is_foot_path = true; + wc.weight = raw_weight; // same units as transit connection weights + append_connection_row(wc, halthandle_t()); + }; + + uint64 total_raw = origin_walk_raw; + + // Origin leg: position → start halt (only when walking and koord given). + if(origin_walk_raw > 0) { + append_pos_row(from_pos); + append_walk_leg(origin_walk_raw); + } + append_halt_row(from_halt); halthandle_t transit_from = from_halt; FOR(vector_tpl, const h, dummy_ware.get_transit_halts()) { - // Fetch the fastest traveler - auto connection = find_connection(transit_from, h, dummy_ware.get_desc()->get_catg_index()); + auto connection = find_connection(transit_from, h, ware_catg_idx); + total_raw += connection.weight; append_connection_row(connection, transit_from); append_halt_row(h); transit_from = h; } + // Destination leg: dest halt → position (only when walking and koord given). + if(dest_walk_raw > 0) { + append_walk_leg(dest_walk_raw); + append_pos_row(dest_pos_for_search); + } + total_raw += dest_walk_raw; + + // Total journey cost row. + result_container.new_component(); + result_container.add_table(2, 1); + { + result_container.new_component("Total:"); + + // Apply the same unit conversion used by append_connection_row. + uint64 display_total; + if(tbgr) { + display_total = total_raw * world()->get_settings().get_spacing_shift_divisor() + / world()->ticks_per_world_month; + } else { + display_total = total_raw; + } + char buf[32]; + snprintf(buf, sizeof(buf), "<%llu>", (unsigned long long)display_total); + auto *lbl = result_container.new_component(); + lbl->buf().append(buf); + } + result_container.end_table(); + reset_min_windowsize(); } @@ -367,4 +550,11 @@ void route_search_frame_t::swap_halt_inputs() { strcpy(dest_halt_input_text, temp); from_halt_input.set_text(from_halt_input_text, lengthof(from_halt_input_text)); dest_halt_input.set_text(dest_halt_input_text, lengthof(dest_halt_input_text)); + + char temp2[64]; + strcpy(temp2, from_koord_text); + strcpy(from_koord_text, dest_koord_text); + strcpy(dest_koord_text, temp2); + from_koord_input.set_text(from_koord_text, lengthof(from_koord_text)); + dest_koord_input.set_text(dest_koord_text, lengthof(dest_koord_text)); } diff --git a/gui/route_search_frame.h b/gui/route_search_frame.h index 559261fc07..173c52a958 100644 --- a/gui/route_search_frame.h +++ b/gui/route_search_frame.h @@ -2,6 +2,7 @@ #define GUI_ROUTE_SEARCH_FRAME_H #include "../simhalt.h" +#include "../dataobj/koord.h" #include "gui_frame.h" #include "components/action_listener.h" #include "components/gui_aligned_container.h" @@ -11,30 +12,49 @@ #include "components/gui_combobox.h" /** - * Info window for factories + * Route search window. Accepts either halt names or tile coordinates (x,y) as + * from/to endpoints. When a coordinate is given it finds all halts serving + * that tile and passes the coordinate as start_pos so that origin/destination + * walking cost is factored into the route weight. */ class route_search_frame_t : public gui_frame_t, public action_listener_t { private: + // --- halt-name inputs --- gui_textinput_t from_halt_input, dest_halt_input; - button_t search_button, reverse_search_button; gui_label_t from_halt_label, dest_halt_label; - gui_aligned_container_t result_container; char from_halt_input_text[256]; char dest_halt_input_text[256]; + + // --- coordinate inputs (format "x,y"; empty means use halt name) --- + gui_textinput_t from_koord_input, dest_koord_input; + gui_label_t from_koord_label, dest_koord_label; + char from_koord_text[64]; + char dest_koord_text[64]; + + button_t search_button, reverse_search_button; + gui_aligned_container_t result_container; halthandle_t from_halt, dest_halt; button_t bt_show_non_traveled; - + vector_tpl viewable_freight_types; gui_combobox_t freight_type_c; uint8 search_ware_index; + // Parsed from *_koord_text; koord::invalid when the field is empty / invalid. + koord from_koord, dest_koord; + void search_route(); void append_connection_row(haltestelle_t::connection_t connection, halthandle_t from_halt); void append_halt_row(halthandle_t halt); + // Shows a tile coordinate as a plain text row (used for koord-based origin/destination). + void append_pos_row(koord pos); void swap_halt_inputs(); + // Parse "x,y" text into koord; returns koord::invalid on failure. + static koord parse_koord(const char* text); + public: route_search_frame_t(); ~route_search_frame_t(); diff --git a/gui/settings_frame.cc b/gui/settings_frame.cc index c28fdc21ec..57738a42e1 100644 --- a/gui/settings_frame.cc +++ b/gui/settings_frame.cc @@ -5,6 +5,8 @@ #include #include "../simcity.h" +#include "../simhalt.h" +#include "../simworld.h" #include "../sys/simsys.h" #include "simwin.h" @@ -132,6 +134,7 @@ bool settings_frame_t::action_triggered( gui_action_creator_t *comp, value_t ) bool settings_frame_t::infowin_event(const event_t *ev) { if( ev->ev_class == INFOWIN && ev->ev_code == WIN_CLOSE ) { + const bool old_transit_by_foot = sets->is_transit_by_foot(); general.read( sets ); display.read( sets ); routing.read( sets ); @@ -141,6 +144,11 @@ bool settings_frame_t::infowin_event(const event_t *ev) // only the rgb colours have been changed, the colours in system format must be updated env_t_rgb_to_system_colors(); + + // If transit_by_foot changed, all halt connections must be rebuilt. + if( sets->is_transit_by_foot() != old_transit_by_foot ) { + world()->set_schedule_counter(); + } } return gui_frame_t::infowin_event(ev); } diff --git a/gui/settings_stats.cc b/gui/settings_stats.cc index bab73c3647..2f90ea083e 100644 --- a/gui/settings_stats.cc +++ b/gui/settings_stats.cc @@ -266,6 +266,11 @@ void settings_routing_stats_t::init(settings_t const* const sets) SEPERATOR INIT_BOOL( "allow_higher_flight", sets->allow_higher_flight ); INIT_BOOL( "use_route_cache", sets->use_route_cache ); + SEPERATOR + INIT_BOOL( "transit_by_foot", sets->transit_by_foot ); + INIT_NUM( "foot_path_weight", sets->foot_path_weight, 0, 0x7FFFFFFFul, gui_numberinput_t::POWER2, false ); + INIT_NUM( "foot_path_time_ticks", sets->foot_path_time_ticks, 0, 0x7FFFFFFFul, gui_numberinput_t::POWER2, false ); + INIT_BOOL( "walk_cost_to_halt", sets->walk_cost_to_halt ); INIT_END } @@ -317,6 +322,10 @@ void settings_routing_stats_t::read(settings_t* const sets) READ_BOOL_VALUE( sets->allow_higher_flight ); READ_BOOL_VALUE( sets->use_route_cache ); + READ_BOOL_VALUE( sets->transit_by_foot ); + READ_NUM_VALUE( sets->foot_path_weight ); + READ_NUM_VALUE( sets->foot_path_time_ticks ); + READ_BOOL_VALUE( sets->walk_cost_to_halt ); } diff --git a/simcity.cc b/simcity.cc index c504a854a1..d877d6b0db 100644 --- a/simcity.cc +++ b/simcity.cc @@ -1959,7 +1959,7 @@ void stadt_t::step_passagiere() ware_t return_pax(wtyp); // now, finally search a route; this consumes most of the time - int const route_result = haltestelle_t::search_route( &start_halts[0], start_halts.get_count(), welt->get_settings().is_no_routing_over_overcrowding(), pax, &return_pax); + int const route_result = haltestelle_t::search_route( &start_halts[0], start_halts.get_count(), welt->get_settings().is_no_routing_over_overcrowding(), pax, &return_pax, origin_pos); halthandle_t start_halt = return_pax.get_ziel(); if( route_result==haltestelle_t::ROUTE_OK ) { // so we have happy traveling passengers diff --git a/simhalt.cc b/simhalt.cc index c3daad0271..c207c1fdf8 100644 --- a/simhalt.cc +++ b/simhalt.cc @@ -1889,6 +1889,95 @@ sint32 haltestelle_t::rebuild_connections() (consecutive_halts[i].get_count() != max_consecutive_halts_schedule[i] || force_transfer_search); } + + // Add foot-path connections: pax can walk between overlapping nearby stops. + // Only for the pax category, never for mail or goods. + // Scan tiles within coverage area directly rather than iterating all halts. + if( welt->get_settings().is_transit_by_foot() && is_enabled(goods_manager_t::INDEX_PAS) ) { + const uint16 cov = welt->get_settings().get_station_coverage(); + + // Per-candidate tracking: for each nearby halt, keep the tile pair that + // minimises Manhattan walk distance + height difference (= total walk cost). + struct foot_candidate_t { + halthandle_t halt; + uint16 walk_d; // Manhattan 2D distance of best tile pair + uint16 z_diff; // height difference of best tile pair + }; + vector_tpl candidates; + + for( auto const &my_tile : tiles ) { + const koord3d my_pos3d = my_tile.grund->get_pos(); + const koord my_pos = my_pos3d.get_2d(); + + for( sint16 dy = -(sint16)cov; dy <= (sint16)cov; dy++ ) { + for( sint16 dx = -(sint16)cov; dx <= (sint16)cov; dx++ ) { + // Eligibility: Chebyshev distance (square coverage area). + if( max( abs(dx), abs(dy) ) > (sint16)cov ) { continue; } + + const planquadrat_t *plan = welt->access( my_pos + koord(dx, dy) ); + if( !plan ) { continue; } + + for( uint8 i = 0; i < plan->get_boden_count(); i++ ) { + const grund_t *gr = plan->get_boden_bei(i); + const halthandle_t other_halt = gr->get_halt(); + if( !other_halt.is_bound() || other_halt == self ) { continue; } + if( !other_halt->is_enabled(goods_manager_t::INDEX_PAS) ) { continue; } + + const uint16 walk_d = (uint16)( abs(dx) + abs(dy) ); + const uint16 z_diff = (uint16)abs( my_pos3d.z - gr->get_pos().z ); + + // Update or insert candidate entry for this halt. + bool found = false; + for( uint32 ci = 0; ci < candidates.get_count(); ci++ ) { + if( candidates[ci].halt == other_halt ) { + if( walk_d + z_diff < candidates[ci].walk_d + candidates[ci].z_diff ) { + candidates[ci].walk_d = walk_d; + candidates[ci].z_diff = z_diff; + } + found = true; + break; + } + } + if( !found ) { + foot_candidate_t fc; + fc.halt = other_halt; + fc.walk_d = walk_d; + fc.z_diff = z_diff; + candidates.append(fc); + } + } + } + } + } + + // Build connections for all candidates found in the coverage area. + const uint32 base_rc = welt->get_settings().get_foot_path_weight(); + const uint32 base_jt = welt->get_settings().get_foot_path_time_ticks(); + const bool time_based = welt->get_settings().get_time_based_routing_enabled(goods_manager_t::INDEX_PAS); + for( uint32 ci = 0; ci < candidates.get_count(); ci++ ) { + const foot_candidate_t &fc = candidates[ci]; + const uint32 dist = (uint32)(fc.walk_d > 0 ? fc.walk_d : 1u) + fc.z_diff; + const uint32 aggregate_weight = time_based ? base_jt * dist : base_rc * dist; + connection_t foot_conn(fc.halt, aggregate_weight, linehandle_t()); + foot_conn.is_foot_path = true; + connection_t *const existing = staged_all_links[goods_manager_t::INDEX_PAS].connections.insert_unique_ordered( + foot_conn, connection_t::compare); + if( existing && aggregate_weight < existing->weight ) { + existing->weight = aggregate_weight; + existing->is_foot_path = true; + } + } + + // Any halt with foot connections must be a transfer point so Dijkstra will explore + // it as an intermediate hop, even if it carries only one vehicle line. + FOR(vector_tpl, const& c, staged_all_links[goods_manager_t::INDEX_PAS].connections) { + if( c.is_foot_path ) { + staged_all_links[goods_manager_t::INDEX_PAS].is_transfer = true; + break; + } + } + } + return connections_searched; } @@ -1946,6 +2035,29 @@ void haltestelle_t::rebuild_linked_connections() } } } + // Also include foot-adjacent halts so their reverse foot connections are rebuilt too. + if( welt->get_settings().is_transit_by_foot() && is_enabled(goods_manager_t::INDEX_PAS) ) { + const uint16 cov = welt->get_settings().get_station_coverage(); + for( auto const& other : alle_haltestellen ) { + if( !other.is_bound() || other == self ) { continue; } + if( !other->is_enabled(goods_manager_t::INDEX_PAS) ) { continue; } + bool overlaps = false; + for( auto const& my_tile : tiles ) { + const koord my_pos = my_tile.grund->get_pos().get_2d(); + for( auto const& other_tile : other->get_tiles() ) { + if( koord_distance(my_pos, other_tile.grund->get_pos().get_2d()) <= cov ) { + overlaps = true; + break; + } + } + if( overlaps ) { break; } + } + if( overlaps ) { + all.append_unique(other); + } + } + } + FOR(vector_tpl, h, all) { if(h.is_bound()) { h->rebuild_connections(); @@ -1964,8 +2076,8 @@ void haltestelle_t::fill_connected_component(uint8 catg_idx, uint16 comp) FOR(vector_tpl, &c, all_links[catg_idx].connections) { c.halt->fill_connected_component(catg_idx, comp); - // cache the is_transfer value - c.is_transfer = c.halt->is_transfer(catg_idx); + // cache the is_transfer value; foot connections always act as transfer so Dijkstra explores them + c.is_transfer = c.is_foot_path ? true : c.halt->is_transfer(catg_idx); } } @@ -2153,17 +2265,39 @@ uint8 haltestelle_t::last_search_ware_catg_idx = 255; * if USE_ROUTE_SLIST_TPL is defined, the list template will be used. * However, this is about 50% slower. */ -int haltestelle_t::search_route( const halthandle_t *const start_halts, const uint16 start_halt_count, const bool no_routing_over_overcrowding, ware_t &ware, ware_t *const return_ware ) +int haltestelle_t::search_route( const halthandle_t *const start_halts, const uint16 start_halt_count, const bool no_routing_over_overcrowding, ware_t &ware, ware_t *const return_ware, const koord start_pos ) { const uint8 ware_catg_idx = ware.get_desc()->get_catg_index(); const uint8 ware_idx = ware.get_desc()->get_index(); + // Walking cost setup: only for passengers when transit_by_foot is enabled. + // Origin walking cost penalises start halts that are farther from the passenger's building. + // Destination walking cost penalises end halts that are farther from the destination tile. + const bool use_walk_cost = (start_pos != koord::invalid) + && welt->get_settings().is_transit_by_foot() + && welt->get_settings().is_walk_cost_to_halt() + && ware_catg_idx == goods_manager_t::INDEX_PAS; + const bool tbgr_walk = use_walk_cost && welt->get_settings().get_time_based_routing_enabled(ware_catg_idx); + const uint32 walk_factor = tbgr_walk + ? welt->get_settings().get_foot_path_time_ticks() + : welt->get_settings().get_foot_path_weight(); + const koord dest_pos = use_walk_cost ? ware.get_zielpos() : koord::invalid; + + // Returns Manhattan walking cost between two 2D positions; 0 when walking disabled. + auto compute_walk_weight = [&](const koord& from, const koord& to) -> uint32 { + if(!use_walk_cost) return 0; + return koord_distance(from, to) * walk_factor; + }; + // since also the factory halt list is added to the ground, we can use just this ... const planquadrat_t *const plan = welt->access( ware.get_zielpos() ); const halthandle_t *const halt_list = plan->get_haltlist(); // but we can only use a subset of these static vector_tpl end_halts(16); end_halts.clear(); + // parallel walking costs from each end halt to dest_pos (0 when use_walk_cost is false) + static vector_tpl end_halt_walk_costs(16); + end_halt_walk_costs.clear(); // target halts are in these connected components // we start from halts only in the same components static vector_tpl end_conn_comp(16); @@ -2190,6 +2324,7 @@ int haltestelle_t::search_route( const halthandle_t *const start_halts, const ui } } end_halts.append(halt); + end_halt_walk_costs.append( compute_walk_weight(halt->get_init_pos(), dest_pos) ); // check connected component of target halt uint16 endhalt_conn_comp = halt->all_links[ware_catg_idx].catg_connected_component; @@ -2211,7 +2346,8 @@ int haltestelle_t::search_route( const halthandle_t *const start_halts, const ui // we already set getoff stop (because this goods is dummy!) // e.g. called by route_search_frame_t halthandle_t halt = ware.get_ziel(); - end_halts.append(halt); + end_halts.append(halt); + end_halt_walk_costs.append( compute_walk_weight(halt->get_init_pos(), dest_pos) ); // check connected component of target halt uint16 endhalt_conn_comp = halt->all_links[ware_catg_idx].catg_connected_component; @@ -2274,18 +2410,32 @@ int haltestelle_t::search_route( const halthandle_t *const start_halts, const ui // this start halt will not lead to any target continue; } - open_list.insert( route_node_t(start_halt, 0) ); + // Walking distance from origin building to this start halt. + // Using max(1u, ...) so best_weight never equals 0 (which is the closed-list sentinel). + // When use_walk_cost is false the walk weight is 0, giving best_weight=1 — negligible + // compared to typical connection weights and preserves backward-compatible behaviour. + const uint32 origin_walk_w = max(1u, compute_walk_weight(start_pos, start_halt->get_init_pos())); + open_list.insert( route_node_t(start_halt, origin_walk_w) ); halt_data_t & start_data = halt_data[ start_halt.get_id() ]; - start_data.best_weight = UINT32_MAX; - start_data.destination = 0; - start_data.depth = 0; - start_data.overcrowded = false; // start halt overcrowding is handled by routines calling this one - start_data.transfer = halthandle_t(); + start_data.best_weight = origin_walk_w; + start_data.destination = 0; + start_data.depth = 0; + start_data.overcrowded = false; // start halt overcrowding is handled by routines calling this one + start_data.transfer = halthandle_t(); + start_data.arrived_by_foot = false; markers[ start_halt.get_id() ] = current_marker; } + // Look up pre-computed destination walking cost for a given halt id (0 for non-end halts). + auto get_dest_walk_cost = [&](uint32 halt_id) -> uint32 { + for(uint32 i = 0; i < end_halts.get_count(); i++) { + if(end_halts[i].get_id() == halt_id) return end_halt_walk_costs[i]; + } + return 0u; + }; + // here the normal routing with overcrowded stops is done while (!open_list.empty()) { @@ -2365,6 +2515,11 @@ int haltestelle_t::search_route( const halthandle_t *const start_halts, const ui // (if not, we were just under construction, and will be fine after 16 steps) const uint32 reachable_halt_id = current_conn.halt.get_id(); + // No consecutive foot-path hops: after a walking leg passengers must board a vehicle. + if( current_halt_data.arrived_by_foot && current_conn.is_foot_path ) { + continue; + } + if( markers[ reachable_halt_id ]!=current_marker ) { // Case : not processed before @@ -2373,17 +2528,19 @@ int haltestelle_t::search_route( const halthandle_t *const start_halts, const ui if( current_conn.halt.is_bound() && current_conn.is_transfer && allocation_pointeris_overcrowded( ware_idx ) ); - halt_data[ reachable_halt_id ].best_weight = total_weight; - halt_data[ reachable_halt_id ].destination = 0; - halt_data[ reachable_halt_id ].depth = current_halt_data.depth + 1u; - halt_data[ reachable_halt_id ].transfer = current_node.halt; - halt_data[ reachable_halt_id ].overcrowded = overcrowded_transfer; - overcrowded_nodes += overcrowded_transfer; + halt_data[ reachable_halt_id ].best_weight = total_weight; + halt_data[ reachable_halt_id ].destination = 0; + halt_data[ reachable_halt_id ].depth = current_halt_data.depth + 1u; + halt_data[ reachable_halt_id ].transfer = current_node.halt; + halt_data[ reachable_halt_id ].overcrowded = overcrowded_transfer; + halt_data[ reachable_halt_id ].arrived_by_foot = current_conn.is_foot_path; + overcrowded_nodes += overcrowded_transfer; allocation_pointer++; // as the next halt is not a destination add WEIGHT_MIN @@ -2408,28 +2565,36 @@ int haltestelle_t::search_route( const halthandle_t *const start_halts, const ui uint32 total_weight = current_halt_data.best_weight + current_conn.weight; - if( total_weight create new node and update halt data const bool overcrowded_transfer = no_routing_over_overcrowding && ( current_halt_data.overcrowded || ( !halt_data[reachable_halt_id].destination && current_conn.halt->is_overcrowded( ware_idx ) ) ); - halt_data[ reachable_halt_id ].best_weight = total_weight; + halt_data[ reachable_halt_id ].best_weight = total_weight; // no need to update destination, as halt nature (as destination or transfer) will not change - halt_data[ reachable_halt_id ].depth = current_halt_data.depth + 1u; - halt_data[ reachable_halt_id ].transfer = current_node.halt; - halt_data[ reachable_halt_id ].overcrowded = overcrowded_transfer; - overcrowded_nodes += overcrowded_transfer; + halt_data[ reachable_halt_id ].depth = current_halt_data.depth + 1u; + halt_data[ reachable_halt_id ].transfer = current_node.halt; + halt_data[ reachable_halt_id ].overcrowded = overcrowded_transfer; + halt_data[ reachable_halt_id ].arrived_by_foot = current_conn.is_foot_path; + overcrowded_nodes += overcrowded_transfer; if( halt_data[reachable_halt_id].destination ) { - best_destination_weight = total_weight; + // Use walk-adjusted weight for pruning so routes to nearer end halts win. + best_destination_weight = effective_weight; + open_list.insert( route_node_t(current_conn.halt, effective_weight) ); } else { // as the next halt is not a destination add WEIGHT_MIN // TODO: (TBGR) use estimated time. Should include waiting time total_weight += WEIGHT_MIN; + open_list.insert( route_node_t(current_conn.halt, total_weight) ); } allocation_pointer++; - open_list.insert( route_node_t(current_conn.halt, total_weight) ); } } // else if not in closed list } // for each connection entry @@ -3036,11 +3201,37 @@ std::shared_ptr haltestelle_t::add_goods_to_halt(halt_wait +bool haltestelle_t::is_foot_path_connection(halthandle_t dest, uint8 catg_index) const +{ + for( auto const& conn : all_links[catg_index].connections ) { + if( conn.halt == dest ) { + return conn.is_foot_path; + } + } + return false; +} + + +bool haltestelle_t::try_foot_transit(ware_t &ware, uint8 foot_steps) +{ + if( foot_steps >= 1 ) { return false; } + if( !welt->get_settings().is_transit_by_foot() ) { return false; } + const halthandle_t next = ware.get_zwischenziel(); + if( !next.is_bound() || next == self ) { return false; } + if( !is_foot_path_connection(next, ware.get_desc()->get_catg_index()) ) { return false; } + // Walk pax to the next halt. Pop the foot hop from transit list so next halt + // routes from its own position rather than re-entering liefere_an with stale state. + ware.pop_first_transit_halts(); + next->starte_mit_route(ware, foot_steps + 1); + return true; +} + + /* same as liefere an, but there will be no route calculated, * since it hase be calculated just before * (execption: route contains us as intermediate stop) */ -uint32 haltestelle_t::starte_mit_route(ware_t ware) +uint32 haltestelle_t::starte_mit_route(ware_t ware, uint8 foot_steps) { if(ware.get_ziel()==self) { if( ware.to_factory ) { @@ -3067,6 +3258,11 @@ uint32 haltestelle_t::starte_mit_route(ware_t ware) return ware.menge; } + // If the next hop is a foot-path, walk passengers immediately — no vehicle needed. + if( try_foot_transit(ware, foot_steps) ) { + return ware.menge; + } + // add to internal storage add_goods_to_halt(halt_waiting_goods_t(ware, welt->get_ticks())); @@ -3128,7 +3324,12 @@ dbg->warning("haltestelle_t::liefere_an()","%d %s delivered to %s have no longer return ware.menge; } } - + + // If the (re-)routed next hop is a foot-path, teleport immediately. + if( try_foot_transit(ware) ) { + return ware.menge; + } + // add to internal storage add_goods_to_halt(halt_waiting_goods_t(ware, welt->get_ticks())); diff --git a/simhalt.h b/simhalt.h index 6e6af6143f..60c2b3b3fa 100644 --- a/simhalt.h +++ b/simhalt.h @@ -253,14 +253,16 @@ class haltestelle_t /// directly reachable halt halthandle_t halt; /// best connection weight to reach this destination - uint32 weight:31; + uint32 weight:30; /// is halt a transfer halt bool is_transfer:1; + /// true if this connection is a foot-path (transit by foot), not a vehicle service + bool is_foot_path:1; /// the line or convoy which has the schedule to get to the halt with the best weight traveler_t best_weight_traveler; - connection_t() : weight(0), is_transfer(false), best_weight_traveler(linehandle_t()) { } - connection_t(halthandle_t _halt, uint32 _weight, traveler_t _best_weight_traveler) : halt(_halt), weight(_weight), is_transfer(false), best_weight_traveler(_best_weight_traveler) { } + connection_t() : weight(0), is_transfer(false), is_foot_path(false), best_weight_traveler(linehandle_t()) { } + connection_t(halthandle_t _halt, uint32 _weight, traveler_t _best_weight_traveler) : halt(_halt), weight(_weight), is_transfer(false), is_foot_path(false), best_weight_traveler(_best_weight_traveler) { } bool operator == (const connection_t &other) const { return halt == other.halt; } bool operator != (const connection_t &other) const { return halt != other.halt; } @@ -480,6 +482,13 @@ class haltestelle_t // returns the matching warenziele (goods objectives/destinations) vector_tpl const& get_connections(uint8 const catg_index) const { return all_links[catg_index].connections; } + // returns true if the connection to dest (for the given category) is a foot-path + bool is_foot_path_connection(halthandle_t dest, uint8 catg_index) const; + + // If the ware's next hop is a foot-path, move it immediately to the next halt. + // Returns true if the ware was teleported (caller must not add it to cargo queue). + bool try_foot_transit(ware_t &ware, uint8 foot_steps = 0); + /** * Checks if there is connection for certain freight to the other halt. * @param halt the other halt @@ -502,7 +511,8 @@ class haltestelle_t */ void new_month(); - uint8 get_connection_update_counter() const { return connection_update_counter;} + uint8 get_connection_update_counter() const { return connection_update_counter; } + static uint8 get_connection_update_counter_static() { return connection_update_counter; } private: /* Node used during route search */ @@ -532,6 +542,10 @@ class haltestelle_t uint16 depth:14; bool destination:1; bool overcrowded:1; + // true when this halt was reached via a foot-path connection in the current search. + // Used to block a second consecutive foot-path hop (passengers must board a vehicle + // between any two walking legs). + bool arrived_by_foot; }; // store the best weight so far for a halt, and indicate whether it is a destination @@ -606,7 +620,13 @@ class haltestelle_t * * if avoid_overcrowding is set, a valid route in only found when there is no overflowing stop in between */ - static int search_route( const halthandle_t *const start_halts, const uint16 start_halt_count, const bool no_routing_over_overcrowding, ware_t &ware, ware_t *const return_ware=NULL ); + /** + * @param start_pos 2D origin position (building tile). When valid and transit_by_foot is + * enabled, the Manhattan distance to each start halt is added to the initial + * route weight so that nearby stops are preferred over distant ones. + * The destination walking cost is derived from ware.get_zielpos() internally. + */ + static int search_route( const halthandle_t *const start_halts, const uint16 start_halt_count, const bool no_routing_over_overcrowding, ware_t &ware, ware_t *const return_ware=NULL, const koord start_pos=koord::invalid ); /** * A separate version of route searching code for re-calculating routes @@ -802,7 +822,7 @@ class haltestelle_t * This is used for inital passenger, since they already know a route * @returns amount of goods */ - uint32 starte_mit_route(ware_t ware); + uint32 starte_mit_route(ware_t ware, uint8 foot_steps = 0); const grund_t *find_matching_position(waytype_t wt) const;