Skip to content

Vehicle car data and persistence

This document is the source of truth for owned vehicle data, persistence, loading, migration, and quarantine. Appearance and editor behavior are documented separately in Vehicle customization.

Source locations

  • src/Shared/DataTypes/DatastoreTypes.luau defines canonical CarData and persisted quarantine metadata.
  • src/Server/FrameworkRevamp/MainDatastore/init.luau owns the profile template and invokes vehicle migration before runtime consumers start.
  • src-new/server/library/datastore/vehicle_datastore/persisted_vehicle_migration.luau converts one historical record without mutating it.
  • src-new/server/library/datastore/vehicle_datastore/persisted_vehicle_partition.luau partitions all normal and quarantined records.
  • src/Server/FrameworkRevamp/MainDatastore/VehicleDatastore/init.luau installs the partition and creates garage replication.
  • src-new/server/library/vehicle_customization/migration.luau converts legacy customization into the canonical customization schema.
  • src-new/client/app/components/guis/VehicleJoinNotices/ owns the count-only player notification shown after the main menu.

Core invariants

Everything in profile.Data.Cars is canonical and usable. Anything that cannot become canonical is preserved unchanged in profile.Data.UnavailableCars and retried on future loads.

Consequently:

  • Runtime systems may treat every value in Cars as DatastoreTypes.CarData.
  • Historical or malformed records never enter garage replication, spawning, customization, selling, rewards, or other runtime vehicle systems.
  • A migration failure must not modify or discard the original record.
  • Every migration input appears in exactly one output collection.
  • Vehicle UIDs are unique across usable cars produced by a partition pass.
  • An unavailable record is recoverable; quarantine is not deletion.

Do not broaden runtime CarData to include unavailable records. Unknown historical data belongs at the persistence boundary so normal vehicle consumers only handle supported runtime variation.

Profile collections

Cars contains canonical usable vehicles:

Cars: { DatastoreTypes.CarData }

UnavailableCars contains exact raw records plus stable diagnostic metadata:

UnavailableCars: {
    {
        CarData = rawPersistedRecord,
        Failure = {
            reason = "missing-trim-definition",
            vehicleId = 123,
            schemaVersion = 1,
            detail = nil,
        },
    },
}

Do not use CarsRemoving as quarantine. It represents valid cars participating in the existing removal/accounting workflow, not records that cannot be interpreted.

Canonical CarData

CarData is a discriminated union of supported runtime architectures:

type CarData = LegacyCarData | AssemblyV1CarData

VehicleArchitecture is the persisted discriminator:

  • legacy must not carry Assembly, AssemblyItemsOwned, or AssemblyLiveriesOwned.
  • assembly-v1 must carry an assembly snapshot and both ownership ledgers.

A record saved before the discriminator existed is inferred as assembly-v1 when it has an assembly snapshot and as legacy otherwise. Inference is permitted only when the discriminator is missing. Once present, the discriminator is authoritative; contradictory fields cause quarantine rather than silent reinterpretation.

New records select architecture explicitly. Ordinary reward, admin, event, and legacy dealership paths use the legacy generator. A dealership purchase with a resolved assembly uses the assembly-v1 generator.

Canonical customization

CarData.Customization is the persisted and replicated source of truth for vehicle appearance. Canonical customization has the current SchemaVersion. Runtime UI and server consumers read it directly from CarData; there is no separate customization mirror or read model.

Customization without a schema version is legacy input and is converted during the vehicle migration pass. A non-nil unsupported schema version is quarantined instead of being interpreted as an older schema.

Canonical window tint is stored as the actual tint percentage and must remain within the applicable trim range (globally 5 through 95 percent). Defaults start at the minimum. Historical canonical values outside the range are clamped non-mutatingly during vehicle migration before runtime consumers receive them.

Customization purchases are server-authoritative. A legacy commit contains canonical customization. An assembly-v1 commit also contains the complete assembly root and accessory/livery ownership grants. All consequences are applied through one garage replica write.

Load and migration sequence

After ProfileService reconciliation and any legacy account transfer, vehicle loading runs synchronously before garage replication, global vehicle accounting, parts loading, and datastore patches. UID backfill is part of the successful partition path.

The partition pass consumes both Cars and the raw records inside UnavailableCars. For each input:

  1. Validate that it is a table with a resolvable vehicle ID and trim definition.
  2. Infer or validate its vehicle architecture using local values only.
  3. Derive missing initial assembly ownership only during missing-discriminator inference.
  4. Reuse canonical customization or build canonical customization from legacy fields.
  5. Construct a replacement table without mutating the input.
  6. Generate a UID on the successful replacement when the historical record lacks one.
  7. Reject a later record that duplicates a UID already accepted in the same pass.
  8. Place the record into exactly one output collection.

Only after the complete pass finishes are profile.Data.Cars and profile.Data.UnavailableCars replaced. Garage replication is then created directly from canonical Cars.

Failure and quarantine

Migration returns structured failure data; it does not log or mutate persistence. The datastore boundary creates the quarantine wrapper, installs the completed partition, and reports unavailable records.

Current failure categories include:

  • invalid persisted record;
  • missing vehicle ID;
  • missing trim definition;
  • unknown vehicle architecture;
  • legacy architecture carrying assembly data;
  • assembly-v1 architecture missing required data;
  • malformed assembly data;
  • an unexpected customization or vehicle migration error;
  • unsupported customization schema;
  • duplicate vehicle UID.

Expected failures use stable reason identifiers. Unexpected caught errors may also record a diagnostic detail string so operations can investigate them without allowing one malformed vehicle to block the rest of the profile.

On failure, the player cannot currently use that vehicle. The raw record remains in the profile and is attempted again on every subsequent load. If a removed definition is restored or migration support is added, the next successful pass moves the canonical replacement back into Cars with its original identity and data.

Player notification

Unavailable vehicles have no garage representation. After partitioning, the server sets the player's UnavailableVehicleCount attribute to the number of quarantined records. This count is the only quarantine information exposed to the client; raw records, identities, failure reasons, and diagnostics remain server-only.

After the player closes the main menu for the first time, the vehicle join-notice coordinator displays a saved-data issue notice when the count is greater than zero. It states the number of affected vehicles, confirms that ownership and saved data were preserved, and asks the player to contact support for more details or if the issue persists. It does not identify vehicles or speculate about the cause.

The notice appears on every join while at least one record remains quarantined. Dismissal through either the close button or OKAY is session-local and never changes persisted quarantine data. If auto-sell refund information is also pending, that notice is displayed first and the saved-data notice follows after dismissal; the two must never overlap.

Future unavailable-vehicle UI must continue to avoid exposing quarantine records as CarData or making normal vehicle actions accept them.

Compatibility rules

Persisted vehicle IDs, architecture values, schema versions, part IDs, and option IDs are durable data contracts. Removing or reassigning definitions requires an explicit saved-data compatibility plan.

When changing persisted vehicle data:

  1. Keep historical-field interpretation inside migration modules.
  2. Make the migration non-mutating and return a complete replacement or structured failure.
  3. Do not silently substitute a different vehicle or trim.
  4. Preserve failed raw input exactly.
  5. Add the new field to the profile template when it is a profile-level collection.
  6. Ensure old and quarantined records can be retried idempotently.
  7. Document the new invariant or compatibility rule here.

Legacy fields should be accompanied by a comment explaining their historical meaning and canonical interpretation. One unavailable vehicle must never prevent the rest of the player's profile from loading.