Index: .github/workflows/run-tests.yml
===================================================================
--- .github/workflows/run-tests.yml	(revision 12173)
+++ .github/workflows/run-tests.yml	(working copy)
@@ -34,6 +34,7 @@
     - name: Build Simutrans
       run: |
         CC="ccache clang" CXX="ccache clang++" make -j$(nproc)
+        CC="ccache clang" CXX="ccache clang++" make -j$(nproc) makeobj
     - name: Install pak64
       run: |
         pushd simutrans
Index: documentation/vehicle-dat-speed-bonus.md
===================================================================
--- documentation/vehicle-dat-speed-bonus.md	(nonexistent)
+++ documentation/vehicle-dat-speed-bonus.md	(working copy)
@@ -0,0 +1,35 @@
+# Individual vehicle speed bonus parameters
+
+Vehicle `.dat` files may override the reference speed and maximum speed-related
+payment used for cargo carried by that vehicle:
+
+```text
+speed_bonus_reference_percent=100
+speed_bonus_max_percent=0
+```
+
+`speed_bonus_reference_percent` is the percentage of the current era and
+waytype reference speed used to calculate the speed bonus. It accepts values
+from 1 to 65535 and defaults to 100.
+
+`speed_bonus_max_percent` caps the speed-related payment factor after the
+vehicle reference speed has been applied. It accepts values from 0 to 65535;
+zero, the default, means no vehicle-specific cap. The global
+`bonus_basefactor` remains the lower limit.
+
+For example, a city bus whose fare should be calculated against half of the
+road reference speed and capped at 70% uses:
+
+```text
+speed_bonus_reference_percent=50
+speed_bonus_max_percent=70
+```
+
+The parameters belong to the vehicle carrying the cargo. In a train, each
+passenger or freight car therefore applies its own values; a locomotive with
+no cargo does not affect the calculation.
+
+MakeObj writes vehicle descriptor version 14 and stores both values for every
+vehicle. When the parameters are omitted, their default values are stored.
+Simutrans continues to read legacy version 13 vehicle descriptors, applying the
+same defaults to them.
Index: simutrans/history.txt
===================================================================
--- simutrans/history.txt	(revision 12173)
+++ simutrans/history.txt	(working copy)
@@ -1,3 +1,4 @@
+	ADD: Optional vehicle-specific reference speed and maximum payment for speed bonus calculations
 	FIX: If factory status label were always on, then factories with stops (like oil rigg and fish swarm) the halt status clashed with factory status. Moreover, the factory name could differ from stop name, so both needs to be show then
 	CHG: using memcpy for image loading (when possible) reduces loading time of paks again
 	ADD: merge stop tool allows to merge with the stop of another player that has set sharing permissions without giving up building and way ownership
Index: src/simutrans/descriptor/reader/vehicle_reader.cc
===================================================================
--- src/simutrans/descriptor/reader/vehicle_reader.cc	(revision 12173)
+++ src/simutrans/descriptor/reader/vehicle_reader.cc	(working copy)
@@ -46,7 +46,33 @@
 
 	vehicle_desc_t *desc = new vehicle_desc_t();
 
-	if (version == 13) {
+	if (version == 14) {
+		// individual speed bonus
+		desc->price              = decode_sint64(p);
+		desc->capacity           = decode_uint16(p);
+		desc->loading_time       = decode_uint32(p);
+		desc->topspeed           = decode_uint16(p);
+		desc->weight             = decode_uint32(p);
+		desc->axle_load          = decode_uint16(p);
+		desc->power              = decode_uint32(p);
+		desc->running_cost       = decode_sint64(p);
+		desc->maintenance        = decode_sint64(p);
+
+		desc->intro_date         = decode_uint16(p);
+		desc->retire_date        = decode_uint16(p);
+		desc->gear               = decode_uint16(p);
+
+		desc->wtyp               = decode_uint8(p);
+		desc->sound              = decode_sint8(p);
+		desc->engine_type        = decode_uint8(p);
+		desc->len                = decode_uint8(p);
+		desc->leader_count       = decode_uint8(p);
+		desc->trailer_count      = decode_uint8(p);
+		desc->freight_image_type = decode_uint8(p);
+		desc->speed_bonus_reference_percent = decode_uint16(p);
+		desc->speed_bonus_max_percent       = decode_uint16(p);
+	}
+	else if (version == 13) {
 		// loading time as uint32
 		desc->price              = decode_sint64(p);
 		desc->capacity           = decode_uint16(p);
Index: src/simutrans/descriptor/vehicle_desc.cc
===================================================================
--- src/simutrans/descriptor/vehicle_desc.cc	(revision 12173)
+++ src/simutrans/descriptor/vehicle_desc.cc	(working copy)
@@ -19,6 +19,8 @@
 #if MSG_LEVEL>0
 	if(capacity) PAKSET_INFO("payload=","%d",capacity);
 	if(loading_time!=1000) PAKSET_INFO("loading_time=","%d",loading_time);
+	if(speed_bonus_reference_percent!=100) PAKSET_INFO("speed_bonus_reference_percent=","%d",speed_bonus_reference_percent);
+	if(speed_bonus_max_percent!=0) PAKSET_INFO("speed_bonus_max_percent=","%d",speed_bonus_max_percent);
 	if(weight) PAKSET_INFO("weight=","%d",weight/1000);
 	if(power) {
 		PAKSET_INFO("power=","%d",power);
@@ -55,6 +57,8 @@
 #endif
 	chk->input(capacity);
 	chk->input(loading_time);
+	chk->input(speed_bonus_reference_percent);
+	chk->input(speed_bonus_max_percent);
 	chk->input(weight);
 	chk->input(power);
 	chk->input(running_cost);
Index: src/simutrans/descriptor/vehicle_desc.h
===================================================================
--- src/simutrans/descriptor/vehicle_desc.h	(revision 12173)
+++ src/simutrans/descriptor/vehicle_desc.h	(working copy)
@@ -67,6 +67,8 @@
 	sint64 running_cost = 0;
 	uint16 capacity     = 0;
 	uint32 loading_time = 1000; // time per full loading/unloading
+	uint16 speed_bonus_reference_percent = 100; // 1..65535, percentage of the waytype reference speed
+	uint16 speed_bonus_max_percent       = 0;   // 0: no vehicle-specific cap; otherwise 1..65535 percent
 
 	uint16 gear = 64;       // engine gear (power multiplier), 64=100
 
@@ -239,6 +241,8 @@
 
 	uint16 get_capacity() const { return capacity; }
 	uint32 get_loading_time() const { return loading_time; } // ms per full loading/unloading
+	uint16 get_speed_bonus_reference_percent() const { return speed_bonus_reference_percent; }
+	uint16 get_speed_bonus_max_percent() const { return speed_bonus_max_percent; }
 	uint32 get_weight() const { return weight; }
 	uint32 get_power() const { return power; }
 	sint64 get_running_cost() const { return running_cost; }
Index: src/simutrans/descriptor/writer/vehicle_writer.cc
===================================================================
--- src/simutrans/descriptor/writer/vehicle_writer.cc	(revision 12173)
+++ src/simutrans/descriptor/writer/vehicle_writer.cc	(working copy)
@@ -61,8 +61,11 @@
 	int i;
 	uint8  uv8;
 
-	uint32 total_len = 57; // version 13 size
+	const uint16 speed_bonus_reference_percent = obj.get_int_clamped("speed_bonus_reference_percent", 100, 1, 65535);
+	const uint16 speed_bonus_max_percent = obj.get_int_clamped("speed_bonus_max_percent", 0, 0, 65535);
 
+	uint32 total_len = 61; // version 14 size
+
 	// must be done here since it may affect the len of the header!
 	string sound_str = ltrim( obj.get("sound") );
 	sint8 sound_id = NO_SOUND;
@@ -88,7 +91,7 @@
 
 	write_name_and_copyright(fp, node, obj);
 
-	node.write_version(fp, 13);
+	node.write_version(fp, 14);
 
 	// Price of this vehicle in cent
 	const sint64 price = obj.get_int64("cost", 0);
@@ -329,6 +332,8 @@
 	node.write_sint8(fp, leader_count);
 	node.write_sint8(fp, trailer_count);
 	node.write_uint8(fp, (uint8) freight_image_type);
+	node.write_uint16(fp, speed_bonus_reference_percent);
+	node.write_uint16(fp, speed_bonus_max_percent);
 
 	sint8 sound_str_len = sound_str.size();
 	if (sound_str_len > 0) {
Index: src/simutrans/gui/convoi_detail.cc
===================================================================
--- src/simutrans/gui/convoi_detail.cc	(revision 12173)
+++ src/simutrans/gui/convoi_detail.cc	(working copy)
@@ -63,7 +63,7 @@
 			// cnv_kmh == SPEED_UNLIMITED means that meaningful revenue
 			// cannot be calculated yet (e.g. vehicle in depot or stopped at station)
 			if(v->get_cargo_max() > 0 && cnv_kmh != SPEED_UNLIMITED) {
-				max_income += (v->get_cargo_max() * ware_t::calc_revenue(v->get_cargo_type(), v->get_waytype(), cnv_kmh))/3000;
+				max_income += (v->get_cargo_max() * ware_t::calc_revenue(v->get_cargo_type(), v->get_waytype(), cnv_kmh, v->get_desc()))/3000;
 			}
 			add_table(2,1);
 			{
@@ -79,6 +79,16 @@
 				l->buf().printf("%s%s", translator::translate("Loading time:"), difftick_to_string(v->get_desc()->get_loading_time(),false) );
 				l->update();
 			}
+			if (v->get_desc()->get_speed_bonus_reference_percent() != 100) {
+				l = new_component<gui_label_buf_t>();
+				l->buf().printf(translator::translate("Fare reference speed: %u%% of era speed"), v->get_desc()->get_speed_bonus_reference_percent());
+				l->update();
+			}
+			if (v->get_desc()->get_speed_bonus_max_percent() != 0) {
+				l = new_component<gui_label_buf_t>();
+				l->buf().printf(translator::translate("Maximum speed payment: %u%%"), v->get_desc()->get_speed_bonus_max_percent());
+				l->update();
+			}
 			// power
 			if(v->get_desc()->get_power()>0) {
 				l = new_component<gui_label_buf_t>();
Index: src/simutrans/gui/depot_frame.cc
===================================================================
--- src/simutrans/gui/depot_frame.cc	(revision 12173)
+++ src/simutrans/gui/depot_frame.cc	(working copy)
@@ -127,6 +127,8 @@
 	LB_VEH_CAPACITY,
 	LB_VEH_DATE,
 	LB_VEH_SPEED,
+	LB_VEH_SPEED_BONUS_REFERENCE,
+	LB_VEH_SPEED_BONUS_MAX,
 	LB_VEH_AUTHOR,
 	LB_VEH_POWER,
 	LB_VEH_VALUE,
@@ -376,6 +378,8 @@
 	cont_vehicle_labels->add_component(labels[LB_VEH_LOADINGTIME]);
 	cont_vehicle_labels->add_component(labels[LB_VEH_SPEED]);
 	cont_vehicle_labels->add_component(labels[LB_VEH_DATE]);
+	cont_vehicle_labels->add_component(labels[LB_VEH_SPEED_BONUS_REFERENCE]);
+	cont_vehicle_labels->add_component(labels[LB_VEH_SPEED_BONUS_MAX]);
 	cont_vehicle_labels->add_component(labels[LB_VEH_POWER]);
 	cont_vehicle_labels->add_component(labels[LB_VEH_VALUE]);
 	cont_vehicle_labels->add_component(labels[LB_VEH_AUTHOR]);
@@ -1524,6 +1528,18 @@
 		}
 
 		labels[LB_VEH_SPEED]->buf().printf( "%s %3d km/h\n", translator::translate("Max. speed:"), veh_type->get_topspeed() );
+		if( veh_type->get_speed_bonus_reference_percent() != 100 ) {
+			labels[LB_VEH_SPEED_BONUS_REFERENCE]->buf().printf( translator::translate("Fare reference speed: %u%% of era speed"), veh_type->get_speed_bonus_reference_percent() );
+		}
+		else {
+			labels[LB_VEH_SPEED_BONUS_REFERENCE]->buf().clear();
+		}
+		if( veh_type->get_speed_bonus_max_percent() != 0 ) {
+			labels[LB_VEH_SPEED_BONUS_MAX]->buf().printf( translator::translate("Maximum speed payment: %u%%"), veh_type->get_speed_bonus_max_percent() );
+		}
+		else {
+			labels[LB_VEH_SPEED_BONUS_MAX]->buf().clear();
+		}
 
 		if(  veh_type->get_power() > 0  ) {
 			if(  veh_type->get_gear() != 64  ){
Index: src/simutrans/gui/vehiclelist_frame.cc
===================================================================
--- src/simutrans/gui/vehiclelist_frame.cc	(revision 12173)
+++ src/simutrans/gui/vehiclelist_frame.cc	(working copy)
@@ -77,6 +77,14 @@
 		part1.printf("%s%s\n", translator::translate("Loading time:"), difftick_to_string(veh->get_loading_time(), false));
 	}
 	part1.printf( "%s %3d km/h\n", translator::translate( "Max. speed:" ), veh->get_topspeed() );
+	if( veh->get_speed_bonus_reference_percent() != 100 ) {
+		part1.printf( translator::translate( "Fare reference speed: %u%% of era speed" ), veh->get_speed_bonus_reference_percent() );
+		part1.append( "\n" );
+	}
+	if( veh->get_speed_bonus_max_percent() != 0 ) {
+		part1.printf( translator::translate( "Maximum speed payment: %u%%" ), veh->get_speed_bonus_max_percent() );
+		part1.append( "\n" );
+	}
 	if( veh->get_power() > 0 ) {
 		if( veh->get_gear() != 64 ) {
 			part1.printf( "%s %4d kW (x%0.2f)\n", translator::translate( "Power:" ), veh->get_power(), veh->get_gear() / 64.0 );
Index: src/simutrans/script/api/api_obj_desc.cc
===================================================================
--- src/simutrans/script/api/api_obj_desc.cc	(revision 12173)
+++ src/simutrans/script/api/api_obj_desc.cc	(working copy)
@@ -82,6 +82,12 @@
 }
 
 
+sint64 calc_revenue_without_vehicle(const goods_desc_t *desc, waytype_t wt, sint32 speedkmh)
+{
+	return ware_t::calc_revenue(desc, wt, speedkmh, NULL);
+}
+
+
 bool building_enables(const building_desc_t* desc, uint8 which)
 {
 	return desc ? desc->get_enabled() & which : 0;
@@ -793,7 +799,18 @@
 	 * @param speedkmh actual achieved speed in km/h
 	 * @returns revenue
 	 */
-	register_method(vm, &ware_t::calc_revenue, "calc_revenue", true);
+	register_method(vm, &calc_revenue_without_vehicle, "calc_revenue", true);
+	/**
+	 * Calculates transport revenue per tile and freight unit using the speed
+	 * bonus settings of a specific vehicle descriptor.
+	 * Value contains an additional factor of 3000. Divide by 3000 *after*
+	 * calculating revenue for a loaded convoy.
+	 * @param wt waytype of vehicle
+	 * @param speedkmh actual achieved speed in km/h
+	 * @param vehicle vehicle carrying the freight
+	 * @returns revenue
+	 */
+	register_method(vm, &ware_t::calc_revenue, "calc_revenue_for_vehicle", true);
 	end_class(vm);
 
 	/**
Index: src/simutrans/simware.cc
===================================================================
--- src/simutrans/simware.cc	(revision 12173)
+++ src/simutrans/simware.cc	(working copy)
@@ -17,6 +17,7 @@
 #include "dataobj/koord.h"
 
 #include "descriptor/goods_desc.h"
+#include "descriptor/vehicle_desc.h"
 #include "builder/goods_manager.h"
 
 
@@ -173,16 +174,31 @@
 }
 
 
-sint64 ware_t::calc_revenue(const goods_desc_t* desc, waytype_t wt, sint32 speedkmh)
+sint64 ware_t::calc_revenue(const goods_desc_t* desc, waytype_t wt, sint32 speedkmh, const vehicle_desc_t* vehicle_desc)
 {
 	static karte_ptr_t welt;
 
-	const sint32 ref_kmh = welt->get_average_speed( wt );
-	const sint32 kmh_base = (100 * speedkmh) / ref_kmh - 100;
+	if (vehicle_desc == NULL ||
+		(vehicle_desc->get_speed_bonus_reference_percent() == 100 && vehicle_desc->get_speed_bonus_max_percent() == 0)) {
+		const sint32 ref_kmh = welt->get_average_speed( wt );
+		const sint32 kmh_base = (100 * speedkmh) / ref_kmh - 100;
 
-	const sint64 grundwert128    = welt->get_settings().get_bonus_basefactor(); // minimal bonus factor
-	const sint64 grundwert_bonus = 1000 + kmh_base*desc->get_speed_bonus();     // speed bonus factor
+		const sint64 grundwert128    = welt->get_settings().get_bonus_basefactor(); // minimal bonus factor
+		const sint64 grundwert_bonus = 1000 + kmh_base*desc->get_speed_bonus();     // speed bonus factor
 
+		return desc->get_value() * std::max(grundwert128, grundwert_bonus);
+	}
+
+	const sint32 waytype_ref_kmh = welt->get_average_speed(wt);
+	const sint32 ref_kmh = std::max(1, (sint32)((sint64)waytype_ref_kmh * vehicle_desc->get_speed_bonus_reference_percent() / 100));
+	const sint64 kmh_base = ((sint64)100 * speedkmh) / ref_kmh - 100;
+
+	const sint64 grundwert128 = welt->get_settings().get_bonus_basefactor();
+	sint64 grundwert_bonus = 1000 + kmh_base * desc->get_speed_bonus();
+	if (vehicle_desc->get_speed_bonus_max_percent() > 0) {
+		grundwert_bonus = std::min(grundwert_bonus, (sint64)vehicle_desc->get_speed_bonus_max_percent() * 10);
+	}
+
 	return desc->get_value() * std::max(grundwert128, grundwert_bonus);
 }
 
Index: src/simutrans/simware.h
===================================================================
--- src/simutrans/simware.h	(revision 12173)
+++ src/simutrans/simware.h	(working copy)
@@ -15,6 +15,7 @@
 class goods_manager_t;
 class karte_t;
 class player_t;
+class vehicle_desc_t;
 
 
 /// Class to handle goods packets (and their destinations)
@@ -127,8 +128,10 @@
 	 * @param desc the freight
 	 * @param wt waytype of vehicle
 	 * @param speedkmh actual achieved speed in km/h
+	 * @param vehicle_desc vehicle carrying the goods, or NULL for the legacy
+	 * calculation without vehicle-specific settings
 	 */
-	static sint64 calc_revenue(const goods_desc_t *desc, waytype_t wt, sint32 speedkmh);
+	static sint64 calc_revenue(const goods_desc_t *desc, waytype_t wt, sint32 speedkmh, const vehicle_desc_t *vehicle_desc = NULL);
 
 	/// Adds the number of goods to this goods packet.
 	/// @return Any excess goods that could not be added, eg due to logical limits.
Index: src/simutrans/vehicle/vehicle.cc
===================================================================
--- src/simutrans/vehicle/vehicle.cc	(revision 12173)
+++ src/simutrans/vehicle/vehicle.cc	(working copy)
@@ -733,7 +733,7 @@
 
 		// calculate freight revenue incl. speed-bonus
 		if (ware.get_desc() != last_freight) {
-			freight_revenue = ware_t::calc_revenue(ware.get_desc(), get_desc()->get_waytype(), cnv_kmh);
+			freight_revenue = ware_t::calc_revenue(ware.get_desc(), get_desc()->get_waytype(), cnv_kmh, get_desc());
 			last_freight = ware.get_desc();
 		}
 		const sint64 price = freight_revenue * (sint64)dist * (sint64)ware.amount;
Index: tests/pak/vehicle-speed-bonus-test.dat
===================================================================
--- tests/pak/vehicle-speed-bonus-test.dat	(nonexistent)
+++ tests/pak/vehicle-speed-bonus-test.dat	(working copy)
@@ -0,0 +1,27 @@
+obj=vehicle
+name=SpeedBonusTest50_70
+waytype=road
+cost=0
+weight=1
+runningcost=0
+engine_type=diesel
+speed=50
+speed_bonus_reference_percent=50
+speed_bonus_max_percent=70
+power=100
+gear=100
+freight=Passagiere
+payload=1
+intro_year=1900
+retire_year=2999
+Constraint[Prev][0]=none
+Constraint[Next][0]=none
+
+EmptyImage[W]=../../themes.src/flat/button.0.0
+EmptyImage[NW]=../../themes.src/flat/button.0.0
+EmptyImage[N]=../../themes.src/flat/button.0.0
+EmptyImage[NE]=../../themes.src/flat/button.0.0
+EmptyImage[E]=../../themes.src/flat/button.0.0
+EmptyImage[SE]=../../themes.src/flat/button.0.0
+EmptyImage[S]=../../themes.src/flat/button.0.0
+EmptyImage[SW]=../../themes.src/flat/button.0.0
Index: tests/tests/test_good.nut
===================================================================
--- tests/tests/test_good.nut	(revision 12173)
+++ tests/tests/test_good.nut	(working copy)
@@ -37,6 +37,15 @@
 
 	ASSERT_TRUE(concrete.calc_revenue(wt_road, 0) == concrete.calc_revenue(wt_road, 999)) // this has a speed bonus of 0
 
+	local custom_vehicle = vehicle_desc_x("SpeedBonusTest50_70")
+	local legacy_city_revenue = pax.calc_revenue(wt_road, 25)
+	local custom_city_revenue = pax.calc_revenue_for_vehicle(wt_road, 25, custom_vehicle)
+	ASSERT_TRUE(custom_city_revenue > legacy_city_revenue)
+
+	local custom_fast_revenue = pax.calc_revenue_for_vehicle(wt_road, 250, custom_vehicle)
+	local custom_faster_revenue = pax.calc_revenue_for_vehicle(wt_road, 999, custom_vehicle)
+	ASSERT_EQUAL(custom_fast_revenue, custom_faster_revenue)
+
 	local wtypes = [ wt_road, wt_rail, wt_water, wt_monorail, wt_maglev, wt_narrowgauge ]
 	local goods = [ pax, concrete ]
 
@@ -48,6 +57,17 @@
 				ASSERT_TRUE(current >= last)
 				last = current
 			}
+
+			// Legacy vehicle descriptors with default individual settings must
+			// produce exactly the legacy revenue.
+			foreach (vehicle in vehicle_desc_x.get_available_vehicles(wt)) {
+				if (vehicle.get_name() == custom_vehicle.get_name()) {
+					continue
+				}
+				foreach (test_speed in [0, 25, 100, 250, 999]) {
+					ASSERT_EQUAL(g.calc_revenue_for_vehicle(wt, test_speed, vehicle), g.calc_revenue(wt, test_speed))
+				}
+			}
 		}
 	}
 }
Index: tools/run-automated-tests.sh
===================================================================
--- tools/run-automated-tests.sh	(revision 12173)
+++ tools/run-automated-tests.sh	(working copy)
@@ -1,7 +1,15 @@
 #!/bin/bash
 set -e
 
+repo_dir=$(pwd)
+mkdir -p ~/simutrans/addons/pak
+pushd tests/pak
+"$repo_dir/build/default/makeobj/makeobj" pak64 \
+	~/simutrans/addons/pak/vehicle.SpeedBonusTest50_70.pak \
+	vehicle-speed-bonus-test.dat
+popd
 
+
 pushd simutrans
 ../sim -use_workdir -objects pak -lang en -scenario automated-tests -addons -debug 2 2>&1 | ts -s | tee output.log &
 pid=$!

