Stops GTA V from parking identical cars next to each other in ambient traffic.
Single player only. See Safety.
Draft. The explanation below is a hypothesis that fits the evidence, not documentation of the engine. Everything in What the binary says is measurable on your own copy of the game, and I would rather be corrected than believed.
Identical cars turning up together is not new and not specific to any one install. It is a long-running trait of RAGE, and the community explanation has been stable for years: the engine keeps a bounded pool of vehicle models in memory, and under pressure it would rather reuse a model it already has than stream in a new one.
GTA IV showed the loud version of this: the city-wide flood of identical taxis that everyone who modded that game remembers. The account that stuck is that its vehicle budget was small and fixed, and once heavy replacement models exhausted it the streamer fell back on whatever was guaranteed to stay resident. I have never verified that mechanism and I am repeating it as received wisdom, but the symptom was real enough that a generation of mods existed to raise the budget.
GTA V swapped the fixed budget for zone-based pools and added a Population Variety slider, which makes the failure far less dramatic without removing the pressure underneath it.
I hit this in my own game and went looking for an explanation, which is how I found SteXmaN's 2015 GTAForums thread, Traffic diversity problem. His description matches mine closely: the same five to seven models cycling endlessly, heavy on SUVs. He also noted something telling — in an unmodified game the spawn set changes as you move between areas, but with heavy replacement models it largely stops rotating.
Two things are worth separating out of all this:
- The engine reusing the player's own car is one route to the symptom, not the whole of it. It is the explanation you hear most often, and it does happen, but it is not the only way to end up with twins parked at a junction.
- Memory pressure explains why the engine tolerates a duplicate. It does not explain why the duplicate is parked twenty metres from its twin. Those are two different problems, and this mod only addresses the second one.
Watch a busy junction long enough and you will see two or three of the same car sitting next to each other. Not similar — identical, same model, same spot.
My reading of it is that three things line up:
-
The engine picks traffic models one at a time, and each pick only knows about the cars that already exist. Nothing coordinates a pick with the ones made moments earlier nearby.
-
There is a distance rule, and it is switched off.
vehicles.metais widely understood to carry a per-vehicleidenticalModelSpawnDistance, and to leave it at0for most vehicles. A minimum distance of zero is not a rule.This is the weakest link in the chain and I have not verified it. Those files live inside the RPF archives, so checking means opening
vehicles.metain OpenIV and reading the value yourself. Note also that an absent field and a field set to0look identical from outside: every parsable attribute gets a default whether the modder wrote it or not. -
The pool of loaded models is small, bounded by a memory budget rather than by how many cars exist. When only a handful are resident, "spawn a duplicate" beats "spawn nothing".
If that is right, then the fix is not to clean up after a duplicate — it is to refuse the pick that would create one.
None of this needs the mod. It is all measurable with a Python script and the game's own executable.
| Observation | On the Steam build I tested (July 2026) |
|---|---|
| Code sections on disk | Entropy 8.00, byte histogram flat — encrypted |
.rdata |
Entropy 5.40 — plain, readable |
The float 0.19 |
Appears exactly once in the whole binary |
The float 90000 |
Appears 6 times. 90000 = 300², which reads like a squared distance |
| Functions referencing both | Exactly one |
That last row is the whole trick. One rare constant plus one corroborating constant is enough to name a single function out of ~91,000, without ever disassembling anything.
The function that comes back is 3126 bytes at RVA 00708134, and its first
bytes back up the guess:
48 89 48 08 mov [rax+08h], rcx ; 1st argument spilled
48 89 50 10 mov [rax+10h], rdx ; 2nd
4C 89 40 18 mov [rax+18h], r8 ; 3rd
44 88 48 20 mov [rax+20h], r9b ; 4th -- a byte, so a bool
then it zeroes two 1000-byte arrays (250 entries of 4 bytes each) and bails out early if a global pointer is null. A model-selection routine that tallies 250 buckets and gives up when its configuration is not loaded is exactly the shape I expected.
Your own log prints all of these numbers, so you can check the reasoning rather than take my word for it.
It sits in front of that function:
- Let the engine make its normal choice.
- If the same model was placed within
SpacingMetersrecently, answer "nothing suitable right now" instead. - The engine skips that spawn and tries again elsewhere a moment later.
Nothing is ever swapped after the fact, because nothing gets built in the first place. That is also why it looks natural — a skipped spawn is something the engine does constantly on its own.
It changes which model gets placed, never what is available to place. If the loaded pool is mostly trucks and SUVs, it stays mostly trucks and SUVs — just no longer the same truck three times at one junction.
That distinction matters because the two problems live in different places. Declining a pick cannot conjure a saloon that is not in memory; refusing everything would only produce empty road. Which classes are resident at all is decided upstream, by population group weightings and how much memory the streamer has to work with — see the last section for the levers that actually move it.
The problem is old and has been attacked before. The closest relative came out of the same thread: Traffic Spawner by alphazolam, which watches for duplicate vehicles and replaces off-screen ones with random models from a list.
It works, and it can do something this mod cannot: fix duplicates that already exist. The trade is inherent to the approach — a car has to be swapped after it has been created, so it needs a minimum distance setting to stop vehicles transforming in view, and it costs frame time to keep scanning.
VaritrafV takes the other end. It declines the pick, so the duplicate is never created and there is nothing to swap: no transformations, and no per-frame work beyond a comparison against the last two dozen spawns. The cost is that it is powerless over traffic that already exists, including everything spawned before it loaded.
Neither is strictly better. If you want existing traffic cleaned up as you drive, use that one. If you want the duplicates not to happen in the first place, use this one.
Needs Visual Studio with Desktop development with C++, and git.
build.bat
Produces build\VaritrafV.asi. MinHook is fetched automatically on first build.
To run the tests:
build.bat test
Visual Studio 18 prints 'vswhere.exe' is not recognized during the build. That
comes from Microsoft's own vcvars64.bat, not from here, and nothing is wrong —
the compiler is found either way.
Copy VaritrafV.asi and VaritrafV.ini next to GTA5.exe, and make sure you
have an ASI loader there too. Any x64 proxy the game actually imports will do —
dinput8.dll is the usual choice.
VaritrafV.ini:
SpacingMeters=70Higher means more variety and slightly thinner traffic.
The log is at %TEMP%\VaritrafV.log:
=== VaritrafV ===
spacing: 70 m
attempt 1
constants: 0.19 x1, 90000 x6
functions: 4 using the first, 10 using the second, 1 using both
found at RVA 00708134, 3126 bytes
+000 48 8B C4 44 88 48 20 4C 89 40 18 48 89 50 10 48
hook active
The number that matters is the last one on the functions: line:
- 1 — identified beyond doubt, hook goes in.
- 0 — the anchors do not match this build; nothing is patched.
- more than 1 — ambiguous, so nothing is patched. Add a third constant in
src/resolve.cppto narrow it down.
After a successful find the log also lists the constants that function reads, sorted rarest first. That list is where the current anchors came from, and where replacements come from if a game update ever breaks them — see Where the anchors come from.
attempt 1 finding nothing is normal. The code sections are encrypted until the
anti-tamper layer unpacks them shortly after launch.
Once traffic is spawning, a line appears every 30 seconds:
spawns: 96 allowed, 14 refused (13%)
That is the only thing proving the mod does work rather than just sitting there.
The counts are cumulative since launch, so the first reading is dominated by the initial population fill and reads much higher than the steady state. On my run at 70 m it went 59% in the first busy window and settled around 28%, and traffic did not visibly thin out. Near zero would mean the spacing never triggers at all.
One caveat on reading it: a refusal is not the same as a duplicate prevented. After a refusal the engine simply tries again, and if it offers the same model nearby a moment later that is counted a second time. The number tells you the veto is firing, not how many twins it saved you from.
The code is encrypted on disk, so there is nothing to scan for in the file. It is decrypted in memory — it has to be, to run — and this mod runs inside that process, so it reads what the game already unpacked for itself.
That still leaves ~91,000 anonymous functions. The way in is that .rdata, where
float constants live, is not encrypted. Scanning for RIP-relative references to
the two constants above narrows it to one function, and
RtlLookupFunctionEntry turns that address into an entry point.
No hardcoded offsets, so a game patch does not break it: 0.19 is still 0.19,
and the search finds the function wherever it moved to.
Two arbitrary-looking numbers deserve better than "trust me", so here is the path to them. It is worth reading not for provenance but because it is the procedure you would need if an update ever breaks the current pair.
Start from the symptom. Cars are ending up too close to identical ones, so
somewhere there is a distance check. Games compare squared distances to avoid a
square root, so squares of round, human-chosen distances are worth a look. In
.rdata, which is not encrypted, that narrows things a lot — of twenty round
distances I tried, fifteen were rare enough to anchor on:
75 m -> 5625 x1 250 m -> 62500 x1
125 m -> 15625 x1 300 m -> 90000 x6
150 m -> 22500 x5 400 m -> 160000 x1
That is a shortlist of candidates, not an answer. You take one, find the functions that reference it, and look for something shaped like a model picker.
Then let the function tell you the rest. Once you have a candidate, the constants it reads are far more informative than anything you can guess from outside. That is why the log prints them, rarest first:
constants it reads, rarest first (rarer means a sharper anchor):
0.19 x1
90000 x6
0.09 x4
...
0.19 appears once in the entire binary, which makes it a far sharper anchor
than the distance constant that led there — so it becomes the primary and the
squared distance stays on as corroboration.
One thing this is not. You could not have found 0.19 by hunting for rare
constants directly. Of the floats between 0.0001 and 1.0 in .rdata, 6588 are
distinct and 5765 of those occur exactly once — being unique is the normal
case for a tuning value, not a clue. It is only findable once you already have
the function.
Do not run this in GTA Online. Memory patching there is a fast ban.
The mod refuses to install itself if BattlEye is loaded, but that is a backstop, not a licence — launch single player directly.
src/
main.cpp DllMain, startup thread, config
resolve.* finding the function in memory
hook.* the interception itself
dedup.h the spacing rule (pure logic, unit tested)
log.h diagnostics
tests/
test_dedup.cpp the spacing rule
test_resolve.cpp the anchor scan, pointed at the test binary itself
tools/ optional: turn a log hex dump into a byte pattern
Both tests run without the game. The resolve test matters most: it is the only way to find out the scan is broken without waiting on a log file inside GTA.
It is falsifiable, which is the point. If duplicates keep appearing while the log reports a healthy refusal rate, then the pick I am intercepting is not the one creating them, and the explanation above needs revising rather than the spacing value.
The refusal counter also tests assumption #2 sideways. If the engine were already keeping identical models apart, a 70 m veto would almost never have anything to refuse. A refusal rate that is clearly above zero says something was getting through that should not have been — which is what the assumption predicts.
Note that none of this is load-bearing for the mod itself: VaritrafV never reads
identicalModelSpawnDistance. It enforces SpacingMeters and ignores whatever
the engine believes about spacing. The assumption explains the symptom; it is not
a dependency of the fix.
Worth knowing either way: if you only care about a handful of vehicles, you do
not need this mod. Set identicalModelSpawnDistance to something like 100 for
them in vehicles.meta. No code, and it goes at assumption #2 directly.
VaritrafV exists for applying the idea to every model at once without editing
hundreds of entries.
A different complaint, and not one this mod can answer: a junction full of box trucks that all happen to be different models. Nothing here is being duplicated, so there is nothing for the spacing rule to refuse.
The levers for that, cheapest first:
- Population Variety, in the graphics settings. It sets how much room the game gives to keeping distinct models resident, and it is one slider.
popgroups.ymtandpopcycle.dat, which exist precisely to set the mix of classes per area and time of day.- The vehicle memory budget, which decides how many models fit at all.
None of those is an ASI mod, which is the point — it is a data problem with a
data answer. The one argument for solving it in code instead is that a mod
editing popgroups.ymt collides with every other mod editing popgroups.ymt,
and doing it at runtime does not. That may end up being a separate mod.