Skip to content

Feature/gas station repair - #2

Merged
noteMASTER11 merged 14 commits into
noteMASTER11:mainfrom
JamDaBam:feature/gas-station-repair
Aug 10, 2026
Merged

Feature/gas station repair#2
noteMASTER11 merged 14 commits into
noteMASTER11:mainfrom
JamDaBam:feature/gas-station-repair

Conversation

@JamDaBam

@JamDaBam JamDaBam commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a vehicle repair service at the existing gas stations, priced by current vehicle damage, alongside the existing realistic-fuel refuel service. Repairing does not cancel the player's active taxi shift, does not leave the vehicle flipped over, does not top off the fuel tank for free, and cannot run at the same time as refueling.

This PR has been carefully reviewed and tested in-game by the author across all the scenarios below.

Changes

Repair at gas stations

  • Reuses the exact same physical-station detection already used for refuel (freeroam_gasStations.gasStationCenterRadius, hooked into the same refuelCarWrapper/activityGatherWrapper/M.onActivityAcceptGatherData wrappers) rather than a second parallel detection system \E2\80\94 one gas station, two services.
  • Unlike refuel, repair works with no active taxi shift: station detection, damage reads, and the repair session itself are ticked unconditionally in M.onUpdate, not gated behind state.active.
  • Damage is read fresh at both display time and purchase time \E2\80\94 while a shift is active from the live telemetry stream, otherwise via a short on-demand vehicle-telemetry probe (realisticFuel.requestCurrentDamage) \E2\80\94 so the price charged always matches the vehicle's actual current damage, never a stale HUD value.
  • New repairPricing.lua (pure module, unit-tested) converts BeamNG's raw, unbounded beamstate.damage into a 0-100% damage value and then into a repair price, both via the same exponential-saturating curve shape already used by delivery.calculateImpactDamage. Bounds are configurable in config.lua's new M.repair table (minimumRepairPrice, maximumRepairPrice, repairPriceScale, damagePercentScale, minimumRepairableDamagePercent, repairDurationSeconds).
  • Repair spend is tracked per-shift (shiftTracker.recordRepairCost, mirroring the existing fuelCost pattern) and reduces shift net income.
  • New "Repair" section added to the existing fuel-station phone panel (not a new modal), showing damage%, price, and a timed progress bar, following the same visual style as refuel.
  • A free "REPAIR VEHICLE" button was also added to the existing cheat/debug menu for testing, using the same underlying repair primitive with no cost or station requirement.

Shift preservation

Repairing damage requires be:reloadVehicle(0), which fires the same BeamNG reset callback that normally cancels the player's active taxi shift. A one-shot, vehicle-ID-scoped, auto-expiring suppression flag is armed immediately before that call and consumed in handleVehicleReset \E2\80\94 only the exact reset caused by that specific repair is swallowed; manual player resets, vehicle switches, and resets on any other vehicle fall through to the existing, unmodified main behavior.

Bugs found and fixed during in-game testing

  • Broken license plate: be:reloadVehicle(0) respawns the vehicle from its raw .pc file path rather than its already-resolved parts config, which left the license plate CEF texture stuck on "NO TEXTURE" after every repair. Fixed by calling core_vehicles.setPlateText(false, vehicleId) once the vehicle settles \E2\80\94 the same primitive BeamNG's own "License Plate" config field and its tech/research API use to force the plate texture to regenerate without another respawn.
  • Vehicle left flipped over: be:reloadVehicle(0) preserves whatever position/orientation the vehicle had, including upside-down or on its roof. Fixed by queuing recovery.startRecovering()/recovery.stopRecovering() on the vehicle's own Lua VM after reload \E2\80\94 the same pair of calls BeamNG's native "Recover Vehicle" (Home key) uses, placing the vehicle upright at its last tracked safe position (which, for a stationary repair, is right where it already was).
  • Free fuel top-off: the pre-repair energy levels are captured and restored so repairing doesn't also refill the tank, but the vehicle-recovery fix above is itself known to refill the tank again afterward \E2\80\94 and since it runs through a different async channel than the tank restore, there was no guaranteed ordering between the two. Fixed by deferring the tank restoration to the same "vehicle VM stable" checkpoint already used for the plate fix, guaranteeing it's applied last.
  • Refuel and repair usable simultaneously: neither purchase path checked whether the other was already in progress. Both buy buttons are now disabled while the other session is active, with the same check enforced server-side in purchase()/purchaseRepair() as defense in depth.

State-lifecycle safety review of the reset suppression

A full audit (code + git-history) of whether preserving TaxiDriver state across the repair-triggered be:reloadVehicle(0) could leave anything stale \E2\80\94 active trip, queued next-offer, telemetry, cargo mass, autopilot, physical-pickup props \E2\80\94 found the suppression design itself sound (vehicle-ID-scoped, correctly non-one-shot, auto-expiring) and every genuinely vehicle-bound piece of state already reacquired at the "VM settled" checkpoint, except autopilot:

  • Autopilot silently stopped resuming after repair: autopilot:suspend(vehicle, false) (the resume call fired once the vehicle VM settles) only re-issues the native drive route if the service was actually suspended first \E2\80\94 it's a no-op otherwise, and the repair path never suspends autopilot before reloading the vehicle. So if "AI Driver" mode was engaged and not already suspended at the moment of repair, native self-driving would silently stop with no error or HUD change. Fixed by calling autopilot:markRouteDirty() right after the existing resume call once the vehicle settles \E2\80\94 the exact same primitive the pre-existing godMode reset-survival branch already uses to recover from a reset without ever needing to suspend autopilot first.

Active trip and queued-offer state were confirmed safe to preserve as-is: both are pure GE-side data with no vehicle object/VM references.

Architecture review: does this duplicate the existing godMode reset-survival mechanism?

The debug/cheat menu's pre-existing "godMode" toggle also keeps an active shift/trip alive across a vehicle reset, which raised the question of whether repair's dedicated suppression mechanism duplicates it. It does not: godMode is a persisted, global, vehicle-unscoped setting that survives every reset while enabled, with no reconciliation for reload-specific side effects (license plate, fuel tanks, telemetry) because the plain in-place reset/recover it targets never causes them. Repair's mechanism instead has to target one specific, self-caused reset from be:reloadVehicle(0) \E2\80\94 a heavier respawn that does cause those side effects \E2\80\94 without suppressing any other reset on the same vehicle. Temporarily toggling the real godMode setting to reuse its branch was considered and rejected (it's a persisted, UI-visible setting, and doing so would incorrectly swallow unrelated resets during the repair window). The two mechanisms are kept separate and are mutually exclusive gates in handleVehicleReset (repair's check returns before godMode is even evaluated).

The review did find one genuine, small duplication worth fixing: both godMode's branch and the repair-reload settle checkpoint independently reapplied the delivery cargo-mass modifier with the same conditional logic. Extracted into a single shared reapplyDeliveryCargoMass(vehicle) helper reused by both call sites \E2\80\94 a pure, behavior-preserving dedup, no functional change.

Tests

Added to tests/lua/combinatorics.lua (all additive, no existing assertions changed):

  • repairPricing.calculateRepairPrice/calculateDamagePercent \E2\80\94 zero/negative/over-range clamping, monotonic increase across damage levels, custom config bounds, nil-safety.

No new autopilot test was needed: the markRouteDirty() \E2\86\92 next update() reissues the native route behavior used to fix the repair-resume gap was already covered by an existing assertion in the stock-AI-routing test.

Manual verification

All of the following were tested in-game and confirmed working by the author:

  • Repair with no active shift: damage evaluated, price shown, money deducted, damage cleared.
  • Repair during an active shift: shift remains active afterward.
  • Repair during an active passenger/job trip: passenger/job/destination state untouched.
  • A normal manual BeamNG vehicle reset (unrelated to repair) still cancels the shift exactly as before.
  • License plate stays correct/regenerates correctly after repair, across many repeated repairs.
  • A flipped-over vehicle ends up upright at roughly the same position after repair.
  • Fuel level is preserved exactly (not topped off) after repair, including in combination with the flip/recovery fix.
  • Repair and refuel buttons are correctly disabled while the other is in progress.
  • AI Driver mode resumes automatically after a repair (added after the safety review found this gap).

Remaining limitations

  • minimumRepairPrice, maximumRepairPrice, repairPriceScale, and damagePercentScale in config.lua's M.repair table are initial values; further balancing may be wanted once more real-world damage/price data is collected across different vehicles and crash severities.

Additional changes since the initial review

Based on the review and additional testing, the following improvements were added:

Realistic Mode integration

Repair costs are now gated behind state.realisticMode.

  • With Realistic Mode enabled, repair pricing behaves exactly as originally implemented.
  • With Realistic Mode disabled, repairs are free: the HUD displays $0, no money is deducted, and balance checks are skipped, matching the existing non-realistic gameplay behavior.

State lifecycle review

A complete review of the repair-triggered reset confirmed that the reset-suppression logic itself is sound and that vehicle-dependent state is correctly restored after be:reloadVehicle(0).

During the review it initially appeared that AI Driver did not always resume correctly after a repair. Investigation showed that this behavior was not caused by the repair implementation, but by a separate issue in the current experimental AI Driver feature.

The repair implementation still restores AI Driver state by calling autopilot:markRouteDirty() once the repaired vehicle has settled, reusing the same recovery mechanism already used by the existing godMode reset-survival path.

The observed AI Driver behavior was ultimately traced to a separate experimental AI Driver issue, which is tracked independently and is unrelated to this PR.

Architecture review

The repair reset-survival logic was compared against the existing godMode reset-survival implementation.

Although both preserve an active shift across a vehicle reset, they serve different purposes:

  • godMode is a persistent global feature affecting every reset while enabled.
  • Repair only suppresses the single reset caused by its own be:reloadVehicle(0) call and additionally restores reload-specific state such as license plates, fuel level, telemetry, cargo mass, and AI Driver.

The two mechanisms therefore remain separate.

The review did identify one small duplication: both code paths reapplied the delivery cargo-mass modifier using identical logic.

This has been refactored into a shared reapplyDeliveryCargoMass(vehicle) helper without changing behavior.

Additional manual verification

The following scenarios were also verified:

  • With Realistic Mode disabled, repair is free: the HUD shows $0, no balance is deducted, and repairs always proceed.
  • With Realistic Mode enabled, repair pricing and payment continue to work as expected.
  • The repair-specific AI Driver recovery logic was reviewed and verified. The AI Driver behavior originally observed after repair was traced to a separate experimental AI Driver issue unrelated to this PR.

Test suite maintenance

Merging main into this branch exposed LuaJIT's 200-active-local-per-chunk limit in tests/lua/combinatorics.lua, as the repair-pricing tests from this branch and the Connected Phone tests from main together exceeded the limit.

The test file was refactored by wrapping independent assertion groups in do...end blocks so their temporary locals are released immediately after use. This is a pure scoping change only: no assertions were added, removed, reordered, or otherwise modified, and the suite continues to behave identically to before.

JamDaBam and others added 6 commits July 26, 2026 20:03
Extends the existing gas-station refuel interaction with a repair
service priced by current vehicle damage, without cancelling the
active taxi shift when the repair primitive triggers BeamNG's
vehicle-reset callback (suppressed via a narrow, one-shot,
auto-expiring flag scoped to that exact reset event).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
be:reloadVehicle(0) respawns from the vehicle's raw .pc file path rather
than its resolved parts config, which left the license plate CEF texture
stuck on "NO TEXTURE" after every repair. core_vehicles.setPlateText is
the same primitive BeamNG's own Vehicle Config "License Plate" field and
its tech/research API use to force the plate texture to regenerate
without another respawn, so it's now queued once the post-repair vehicle
settles.

Also adds a free "REPAIR VEHICLE" button to the cheat/debug menu for
testing, mirroring the paid gas-station repair primitive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Never read anywhere, unlike the trip.fuelCost pattern it mirrored
(initialized at trip start and consumed by vehicleHistory.recordRide).
Shift-level repair-cost tracking via shiftTracker.recordRepairCost
already covers this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
be:reloadVehicle(0) preserves whatever position/orientation the vehicle
had, so repairing while flipped over/on its roof kept it that way.
Queues recovery.startRecovering()/stopRecovering() on the vehicle's own
Lua VM after reload -- the same pair of calls BeamNG's native "Recover
Vehicle" (Home key) uses -- to place it upright at its last tracked
safe position, which for a stationary repair is right where it already is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tank restoration ran immediately after be:reloadVehicle(0), but the
recovery.startRecovering()/stopRecovering() call added afterward is
known to refill the tank again, and since it goes through a separate
async channel (queueLuaCommand) than the tank-restore's vehicle-bridge
call, there was no guaranteed ordering between the two. Restoration is
now deferred to the same scannerBecameReady checkpoint already used for
the post-repair plate fix, guaranteeing it's applied last.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both actions run at the same gas station and share the same panel, but
neither checked whether the other was already in progress. Buy buttons
are now disabled while the other session is active, and purchase()/
purchaseRepair() reject the same case server-side as defense in depth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@JamDaBam
JamDaBam marked this pull request as draft July 27, 2026 06:34
State-lifecycle safety review found that autopilot's resume call after
a repair reload was a silent no-op unless the service had been suspended
first, so native self-driving would stop without warning after a repair.
Suspend autopilot before be:reloadVehicle(0), matching the pattern used
by every other reset path, and add a regression test for the underlying
suspend/resume no-op behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@JamDaBam
JamDaBam force-pushed the feature/gas-station-repair branch from 080debc to 7767c78 Compare July 27, 2026 07:40
JamDaBam and others added 2 commits July 27, 2026 10:01
Reuses the exact recovery primitive the pre-existing godMode
reset-survival branch already uses (autopilot:markRouteDirty(), which
forces the next autopilot:update() tick to reissue the native route
regardless of prior suspend state) instead of suspending autopilot
before the reload just to make the existing suspend(false) resume call
meaningful. Drops the now-unneeded suspend-before-reload call and its
dedicated regression test, since the markRouteDirty -> reissue behavior
was already covered by an existing assertion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The godMode reset-survival branch and the repair-reload settle
checkpoint each independently reapplied the delivery cargo-mass
modifier with the same conditional logic. Extract it into one shared
helper reused by both call sites instead of duplicating it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@JamDaBam
JamDaBam marked this pull request as ready for review July 27, 2026 20:53
@noteMASTER11

Copy link
Copy Markdown
Owner

Could you please check if this PR fits the 4.0.0-rc? I had lotta stuff to do cause of 0.39 release

@JamDaBam

Copy link
Copy Markdown
Contributor Author

Could you please check if this PR fits the 4.0.0-rc? I had lotta stuff to do cause of 0.39 release

Will check it out.
I work on a companion fix as well.
The map doesn't render in the browser.

@noteMASTER11

Copy link
Copy Markdown
Owner

@JamDaBam I am fixing map canvas in the Connected Phone right now, it will be released as 4.0.1-RC

Pulls in upstream's Release 4.0.0 RC (new UI/CSS design system,
AI/autopilot overhaul, updated visual-test baselines).

Resolved two conflicts:
- scripts/package-mod.ps1: required archive entry count - both
  branches added files independently (this branch: repairPricing.lua,
  60->61; main: aiDriverRoute.lua + icon.jpg, 60->62), combined to 63.
- app.html: main restyled the fuel/repair panel's close and buy
  buttons with the new taxi-action/taxi-action--primary/--tertiary
  classes; kept this branch's repair-station-aware title and
  ng-disabled conditions (blocking refuel while a repair is active)
  alongside main's new classes.

Note: tests/lua/combinatorics.lua fails on an autopilot route-trimming
assertion after this merge - verified this is a pre-existing bug in
upstream main's new AI code (fails identically on plain upstream main
in isolation), not something this branch or merge introduced. This
branch's own tests passed cleanly before the merge.
@JamDaBam

Copy link
Copy Markdown
Contributor Author

@noteMASTER11 i merged and tested this feature.
It's working.

@noteMASTER11

Copy link
Copy Markdown
Owner

@JamDaBam Need some time to test but it's 4 AM for me, need to sleep after making 4.0.0-RC for more than 7 hours in a raw. Sorry but please wait till tomorrow, I'll test your PR and merge it. This idea was one of the underestimated puzzles from my TODOs and always was throwed back before a bit more valuable features

@JamDaBam

JamDaBam commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@noteMASTER11

No problem.
Take your time.
For me it's 2 AM and I'm going to bed as well.

Thanks for your awesome work.
This mod is my favorite.

@noteMASTER11

Copy link
Copy Markdown
Owner

@JamDaBam Today GPT5.6-Sol ExtraHigh loses Claude, because it took too much time for me to find and fix the issues instead of your work. Maybe because of overheated context window which includes about 25K lines changes. Are you using Fable5?

@JamDaBam

Copy link
Copy Markdown
Contributor Author

@noteMASTER11
No I've used Claude Code with standard Sonnet 5.

@JamDaBam
JamDaBam marked this pull request as draft July 30, 2026 14:33
@JamDaBam

Copy link
Copy Markdown
Contributor Author

One additional change I'd like to make before this PR is ready:

When Realistic Mode is disabled, repairing the vehicle at a gas station should be free, matching the existing non-realistic gameplay behavior. Repair costs should only be charged when Realistic Mode is enabled.

I'll add this before considering the feature complete.

JamDaBam added 4 commits July 30, 2026 18:19
Repair cost/balance checks now only apply when state.realisticMode is
true, matching existing non-realistic gameplay behavior for fuel.
…imit

Merging main pulled in another branch's new top-level test locals on top
of this branch's repair-pricing tests, pushing the flat combinatorics.lua
chunk over LuaJIT's 200-active-local cap. Wrap each independent assertion
block (verified to have no references outside its own range) in do...end
so its locals go out of scope before the next section runs.
@JamDaBam
JamDaBam marked this pull request as ready for review July 30, 2026 17:28
@JamDaBam

JamDaBam commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

The feature would be finished and ready for testing.

@noteMASTER11

noteMASTER11 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

@JamDaBam Fixing the car consistently caused the game to crash on the 4.0.1 version of the mod. It was tested on different standard maps. The PR will come out with significant edits on my part.

The repair caused be:reloadVehicle(0), BeamNG started to recreate the current ETK I-Series (etki/2400_A.pc) and dropped to C++ on the main thread inside finishConstructionGESide.

@noteMASTER11

Copy link
Copy Markdown
Owner

For repairs, I found a standard implementation of BeamNG 0.39: the "Repair vehicle" item itself calls resetBrokenFlexMesh() and spawn.safeTeleport(..., resetVehicle=true). This path resets physical damage without destroying and recreating Vehicle Lua — it was the native reboot that fell. I switch the mod to the same mechanism and save the fuel level on top of the reset.

@noteMASTER11

Copy link
Copy Markdown
Owner

Fixed. Will be pushed after merging.

@noteMASTER11
noteMASTER11 merged commit b0ca53d into noteMASTER11:main Aug 10, 2026
Deggory pushed a commit to Deggory/TaxiDriverReloaded that referenced this pull request Aug 11, 2026
Add the complete Brazilian Portuguese translation from issue noteMASTER11#7, extend it with PR noteMASTER11#2 repair strings, and mark the package and external UI cache as a local 2026-08-10 test build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants