nfsu2-re

https://github.com/yugecin/nfsu2-re

blog > Customizing sponsor cars (2023 July 15)

Customizing sponsor cars #

Index

Sponsor cars? #

There are a few cheats that, when triggered, will unlock a sponsor car that you can select when choosing a car for quick races. These are cars with a preset tuning (though it seems like none of them has some performance tuning done). For example, typing the cheat "yodogg" during boot (when you get on the "press enter" screen at start), will unlock Snoop Dogg's sponsor car. For all cheats, see: Cheat configuration data

After at least one of the sponsor car cheats has been triggered, one can select the "SPONSOR CARS" category of cars in quick race. I never noticed this category filter when I was younger, so I never manager to actually see these sponsor cars, since it only shows stock & customized cars by default. None of the guides and cheats pages that I've found has mentioned this, they always just describe the cheat as "sponsor car" and some mention that it is in "quick race", but none mention this category filter thing you have to operate to actually get the cars.

Snoop Dogg's sponsor car shown in quick race car selection
Snoop Dogg's sponsor car

Index

The idea #

Since one can only access these cars during quick race, it's not possible to check their configuration. Body parts can be guessed, but normally it's not possible to see things like the trunk area, with trunk layout and components configuration. They also of course feature vinyls that are not available for the player to use.

So the idea is to be able to select sponsor cars when using the "customization" menu, so one can create a tuned car based off a sponsor car. Then it's possible to see and browse - and even change - the whole configuration.

Index

Execution #

See nfsu2-re-hooks/fun-car-customize-sponsor.c for all the code together.

Execution

Skipping the cheats #

It's annoying to have to type the cheats every time you want to select sponsor cars. Especially since I changed the boot process so that the "press enter" screen is skipped (see Speedy boot).

This can be easily fixed by replacing CheatScreenData::IsSponsorCarCheatTriggered with a function that always returns 1.

static
int 
__stdcall
fun_car_customize_sponsor_replace_CheatScreenData__IsSponsorCarCheatTriggered(unsigned int *sponsorCarHash)
{
	return 1;
}

mkjmp(0x579D70, fun_car_customize_sponsor_replace_CheatScreenData__IsSponsorCarCheatTriggered);

Index

Execution

Allow sponsor cars in customization #

Then it must be possible to choose sponsor cars from the car select menu when customizing. This can be done by hooking into function CarSelectFNGObject::ChangeCategory, which is used to determine what category of cars to show when the prev/next arrow buttons are pressed.

This car selection screen is used for a few different reasons: selecting quick race car, selecting car to customize, selecting car in online mode (not career car select, that one has a dedicated screen). So we're gonna add the sponsor category in the category rotation, but only if car selection is being done for choosing a car to customize:

static
int fun_car_customize_sponsor_CarSelectFNGObject__ChangeCategory(struct CarSelectFNGObject *this, void *_, unsigned int message)
{
#define MSG_PREV 0x5073EF13
#define MSG_NEXT 0xD9FEEC59

	if (profileData->menuState == MENU_STATE_CAR_CUSTOMIZE) {
		if (message == MSG_PREV) {
			switch (carSelectCategory) {
			case IS_STOCK_CAR | IS_TUNED_CAR:
				if (CarSelectFNGObject::CountAvailableCars(this, IS_SPONSOR_CAR)) {
					carSelectCategory = IS_SPONSOR_CAR; break;
				}
			case IS_SPONSOR_CAR:
				if (CarSelectFNGObject::CountAvailableCars(this, IS_TUNED_CAR))) {
					carSelectCategory = IS_TUNED_CAR; break;
				}
			case IS_TUNED_CAR: carSelectCategory = IS_STOCK_CAR; break;
			case IS_STOCK_CAR: carSelectCategory = IS_STOCK_CAR | IS_TUNED_CAR; break;
			}
		} else if (message == MSG_NEXT) {
			switch (carSelectCategory) {
			case IS_STOCK_CAR | IS_TUNED_CAR: carSelectCategory = IS_STOCK_CAR; break;
			case IS_STOCK_CAR:
				if (CarSelectFNGObject::CountAvailableCars(this, IS_TUNED_CAR))) {
					carSelectCategory = IS_TUNED_CAR; break;
				}
			case IS_TUNED_CAR:
				if (CarSelectFNGObject::CountAvailableCars(this, IS_SPONSOR_CAR))) {
					carSelectCategory = IS_SPONSOR_CAR; break;
				}
			case IS_SPONSOR_CAR: carSelectCategory = IS_STOCK_CAR | IS_TUNED_CAR; break;
			}
		}
		profileData->player1[profileData->currentPlayerIndex?].d4.currentCarSelectionCategory = carSelectCategory;
		CarSelectFNGObject::ResetBrowableCars(this);
		CarSelectFNGObject::UpdateUI(this);
		return 1;
	}
	return 0;
}

static
__declspec(naked)
void fun_car_customize_sponsor_CarSelectFNGObject__ChangeCategory_hook(unsigned int message)
{
	_asm {
		push ecx
		call fun_car_customize_sponsor_CarSelectFNGObject__ChangeCategory
		test eax, eax
		jnz ok
		// we were not in customize menu, call the normal function
		// but first do the thing that we overwrote in order to jmp to this proc
		pop ecx
		mov eax, 0x7F444C // carSelectCategory
		mov eax, [eax]
		mov edx, 0x4EED15 // edx can be used because it's overridden later in that proc anyways
		jmp edx
ok:
		add esp, 4
		retn 0x4
	}
}

mkjmp(0x4EED10, fun_car_customize_sponsor_CarSelectFNGObject__ChangeCategory_hook);

This will crash the game when continuing when a sponsor car is selected :D

Index

Execution

Making the game not crash #

Note: following code can be seen in revision c6786b6a7a59e3b31f20a41bcff108d4f2ec9982 but was changed afterwards, this is explained in the last section.

The game crashes now when continuing when a sponsor car is selected, inside CustomizeCar. It will be called with a hash that denotes what car to customize. This could be a stock car slot hash (like hash of STOCK_PEUGEOT) or a tuned car slot hash (like hash of TUNED_CAR_2) or a career car slot hash (like hash of CAREER_SLOT_1) or an online car slot (like hash of ONLINE_CAREER_CIRCUIT), see comment of struct InventoryCar.slotHash.

When selecting a sponsor car, it will use its hash (like hash of SPONSOR_SNOOP_DOGG). The function tried to find a car in the list of stock cars, tuned cars, career cars or online cars. It won't find the sponsor car, and there is no fallback or null checking, so a segfault follows.

To fix that, we can hook at the last place right before the car is used, try to find a sponsor car for that hash, and continue with that one. We need an instance of struct TunedCar, so we can't just return the struct SponsorCar instance, so we're gonna call CarCollection::CreateNewTunedCarFromFromDataAtSlot (this function is used when selecting a stock car to tune) with the stock name hash that the sponsor car is based on.

static
struct TunedCar*
__stdcall
fun_car_customize_sponsor_GetSponsorCar(struct CarCollection *this, unsigned int slotNameHash)
{
	char buf[32];
	int i;

	i = this->numSponsorCars;
	while (i-- > 0) {
		if (this->sponsorCars[i].__parent.slotHash == slotNameHash) {
			sprintf(buf, "STOCK_%s", FindCarPreset(this->sponsorCars[i].carPresetHash)->modelName);
			return CarCollection::CreateNewTunedCarFromFromDataAtSlot(this, cshash(buf));
		}
	}
	return NULL;
}

static
__declspec(naked)
void
fun_car_customize_sponsor_set_car_instance_if_missing()
{
	_asm {
		test esi, esi
		jnz allgood
		// oh yey, no car instance, we're probably trying to customize a sponsor car. LEHDOTHIS
		// only need to retain esi here (value in ebx is not used from the hooked point)
		push ebx // slotNameHash
		push ecx // this
		call fun_car_customize_sponsor_GetSponsorCar
		mov esi, eax
		mov byte ptr [ebp+8+3], 1 // this happens when a new tuned car is created from stock car, so lets do that
allgood:
		mov cl, byte ptr [ebp+8+3] // overwrote this
		push 0 // overwrote this
		mov eax, 0x552DC0
		jmp eax
	}
}

mkjmp(0x552DBB, fun_car_customize_sponsor_set_car_instance_if_missing);

Crash fixed, but now we just get a stock car when we choose to customize a sponsor car.

Index

Execution

Applying sponsor car tuning #

Note: following code can be seen in revision c6786b6a7a59e3b31f20a41bcff108d4f2ec9982 but was changed afterwards, this is explained in the last section.

In CarCollection::CreateNewTunedCarFromFromDataAtSlot, right after the function we hooked in the previous section to fix the crash, the struct TunedCar instance is used to call its function struct InventoryCar$vtable.procApplyTuningToInstance, which I believe is a function that applies tuning of this car to a car instance.

With that in mind, it means that that (alleged) ApplyTuning function is called on the newly created struct TunedCar instance, which - if you remember from previous section - is created from a stock car. So then it makes sense that we just get a fresh stock car.

To fix, let's change the code from the previous section to store the sponsor car that was selected, so that we have that reference to apply tuning.

// assign the sponsorcar to this when returning from fun_car_customize_sponsor_GetSponsorCar
struct SponsorCar *fun_car_customize_sponsor_stored_sponsorcar;

Then hook once more, and call the correct functions (for sponsor cars) with the sponsor car instances if we have one. Otherwise just execute the code that it normally would.

static
__declspec(naked)
void
fun_car_customize_sponsor_apply_sponsor_tuning()
{
	_asm {
		mov eax, [fun_car_customize_sponsor_stored_sponsorcar]
		test eax, eax
		jz wasnotsponsorcar
		//push 0 // already pushed
		//push offset customizingCarInstanceA // already pushed
		push edi // playerIndex
		mov ecx, [fun_car_customize_sponsor_stored_sponsorcar]
		mov eax, offset SponsorCar::ApplyTuningToInstance // vtable func C for sponsor cars
		call eax
		// 2nd car instance
		push 0
		push offset customizingCarInstanceB
		push edi
		mov ecx, [fun_car_customize_sponsor_stored_sponsorcar]
		mov eax, offset SponsorCar::ApplyTuningToInstance // vtable func C for sponsor cars
		call eax
		// reset state (so we don't do this again when another car is selected) and rt
		mov dword ptr [fun_car_customize_sponsor_stored_sponsorcar], 0
		mov eax, 0x552DF2
		jmp eax
wasnotsponsorcar:
		// do what we overwrote and jmp back
		mov edx, [esi]
		push edi
		mov ecx, esi
		mov eax, 0x552DDC
		jmp eax
	}
}

mkjmp(0x552DD7, fun_car_customize_sponsor_apply_sponsor_tuning);

So that... kind of worked. But as long as no component is changed, it still shows the stock car. Interesting behavior. Once changing and saving a component though, it will always correctly show the tuning and the car will be saved to the tuned cars collection.

Index

Execution

Fixing ghost stock car bug #

Note: following code can be seen in revision c6786b6a7a59e3b31f20a41bcff108d4f2ec9982 but was changed afterwards, this is explained in the last section.

Thanks to more findings, I managed to fix the 'bug' from previous section. Function TunedCar18::CopyTuningFromMenuCarInstance seems to apply tuning done to a struct MenuCarInstance back to a struct TunedCar. So we just have to call that function before returning after applying the stock car tuning:

	// copy tuning back from the car instance to the tuned car entry
	push offset customizingCarInstanceB
	lea ecx, [esi+field_18]
	mov eax, offset TunedCar18::CopyTuningFromMenuCarInstance
	call eax

sweet!

Index

Updates #

Updates

Making things simpler #

Thanks to more findings when I managed to fix the ghost stock car bug, I also found TunedCar18::CopyTuningFromMenuCarInstance, so then the idea came up to use that to apply sponsor car tuning immediately when creating the tuned car instance. And that worked!

This makes the code in the last two sections unnecessary. The fun_car_customize_sponsor_GetSponsorCar function also gets a more appropriate name of fun_car_customize_sponsor_CreateTunedCarFromSponsorCar.

static
struct TunedCar*
__stdcall
fun_car_customize_sponsor_CreateTunedCarFromSponsorCar(struct CarCollection *this, unsigned int slotNameHash)
{
	struct MenuCarInstance *menuCarInstance;
	struct SponsorCar *sponsorCar;
	struct TunedCar* tunedCar;
	char buf[32];
	int i;

	i = this->numSponsorCars;
	while (i-- > 0) {
		if (this->sponsorCars[i].__parent.slotHash == slotNameHash) {
			sprintf(buf, "STOCK_%s", FindCarPreset(this->sponsorCars[i].carPresetHash)->modelName);
			tunedCar = CarCollection::CreateNewTunedCarFromFromDataAtSlot(this, cshash(buf));
			// (need a temp instance of MenuCarInstance to use for code below, the customizingCarInstanceB variable
			//  is a simple instance to use because it gets overridden later in the Customize process
			//  anyways so we can do as we please)
			// (4th param is unknown, but unused in SponsorCar's ApplyTuning, so whatever is fine
			SponsorCar::ApplyTuningToInstance(sponsorCar, profileData->currentPlayerIndex?, &customizingCarInstanceB, 0);
			// copy tuning back from MenuCarInstance to tuned car instance
			TunedCar18::CopyTuningFromMenuCarInstance(&tunedCar->field_18, &customizingCarInstanceB);
			return tunedCar;
		}
	}
	return NULL;
}

See nfsu2-re-hooks/fun-car-customize-sponsor.c for all the code together.

Index