OPX//77
opx_infinitydealership

dealership

Last updated on

Vehicle dealers — buy a car from a marker, dress a showroom with locked preview cars, and let a salesperson sell to another player for a company account.

A player stands on a dealer marker and presses the key (E by default): a centred list shows the stock of that dealer's kind, grouped by class, then asks which of their garages to file the car under. The money is taken, the car is registered through vehicles and, by default, handed over on the marker. A dealer can also show locked preview cars, and inside a dealer's zone a salesperson can offer a car to another player, who accepts it on their own screen.

Sideboth
Requirescharacter (hard)
Optionalvehicles, garages, prompts, menu, target, vehiclekeys
Contractdealership v1 — server, client
Configconfig/dealership.lua (shared)
Dataopx77_company_accounts (company balances), opx77_company_pending (deposits still owed); opx77_dealerships, opx77_dealership_previews (legacy, read-only)
  • Without vehicles: nothing can be sold.
  • Without garages: every car is filed under the vehicles DEFAULT_GARAGE; no destination can be chosen.
  • Without menu: no list, but /opx.dealership.buy still works.
  • Without target: no "sell a vehicle" row.
  • With vehiclekeys: the buyer receives a key with every sale (a full bag is said to the buyer and does not undo the sale).

Commands

CommandAccessArgumentsEffect
/opx.dealership.listACL command.opx.dealership.list—Every dealer (kind, label, position, heading, bucket, config or legacy) and every showroom car with whether it is standing.
/opx.dealership.stockeveryone—Every stock row: kind, class, key, label, price.
/opx.dealership.buyeveryone<key> [garage]Buys stock row key at the dealer the caller stands on, filed under garage if given. Same checks as the list. Cooldown COOLDOWN_MS.

command.opx.dealership.* grants all three. Names come from COMMANDS.

How a sale works

  1. The server re-checks everything: the dealer exists; the player is within USE_RADIUS in the same bucket; the dealer's kind sells that row (garage sells ground vehicles, avpad AVs); the currency is valid; the player can afford it. A chosen garage must be a garage of the same kind with a location in the player's bucket.
  2. The price is taken with character.RemoveMoney.
  3. The vehicle is registered with vehicles.Register. On failure (vehicle.limit, …) the price is refunded. If the refund also fails, the log says so and it must be settled by hand.
  4. A key is cut (vehiclekeys.Ensure) when that module runs.
  5. With HAND_OVER = true the car is created on the dealer marker, facing its HEADING (AVs AV_LIFT m higher). If the marker is taken, it tries one, then two car-widths to either side along the heading. All five taken: the sale still goes through and the buyer is told the car waits in its garage (dealership.handOverBlocked). A refused hand-over does not undo the sale: the car stays owned and filed.
  • A refund (failed registration) and the seller's commission are paid to the character, not the connection: offline or switched, it goes through AddMoneyOffline.
  • One purchase per citizen runs at a time (counter, buy command and accepted offer share one claim), so PER_CHARACTER cannot be passed by racing them.
  • The nearest dealer is searched in the player's own bucket only.

Selling to another player

  • Inside ZONE_RADIUS of a dealer, the target eye shows Sell them a vehicle on every other player. The seller's list is titled Sell to the person in front of you (#12); the buyer reads You are being offered a {model} and Offered to you for {price}. No name either way: the offer carries no seller, and the seller is never shown the buyer's account name. The buyer must answer within OFFER_TIMEOUT_MS; both must still be in the zone when they do.
  • The counter's garage row reads Buy, deliver here. A hand-over refused for another reason than "every spot taken" says the car waits in the garage. A zero commission reads Sale done: {model}. The whole price went to the company.
  • An offer that arrives while the buyer has their own dealership list open waits until that list closes. When an offer expires or the seller leaves, the buyer's offer screen closes (opx:net:dealership:withdrawn).
  • At accept the seller's company is read again; a change refuses the sale with dealership.sellerNoCompany.
  • On a yes the buyer picks one of their garages of that kind on their own screen (Decide), resolved in the buyer's bucket; none = the vehicles default garage; an unknown one is refused dealership.noSuchGarage. The buyer pays the full price and the sale runs the steps above.
  • The seller's company receives the price minus the commission; the seller receives SELLER_CUT_PERCENT (rounded down). A commission that cannot be paid to the seller goes to the company.
  • The company is the seller's job (when COMPANY.JOBS is on and the job is not in EXCLUDED), otherwise their gang (when COMPANY.GANGS is on). Neither: refused dealership.noCompany.
  • Balances live in opx77_company_accounts (kind = job or gang, group_key, balance). Deposits only; no withdraw path. Read one with Balance.
  • Every company deposit goes through the ledger opx77_company_pending (id, token UNIQUE, kind, group_key, amount, plate, created_at) under its own token, then is settled at once. A settlement is one transaction: it locks the row, deposits it as an INSERT … SELECT of that row, then deletes it. A row already gone pays nothing, so a deposit whose answer was lost after commit, a retry, or the sweep racing a sale never pays twice. A deposit not settled at once leaves the row and the sale reports pending = true; the sweep (at boot, then every 60 s, 32 rows per pass) settles it. If even the ledger write fails, the deposit is held in memory and retried; a restart in that window loses it (logged as an error, settle by hand).
  • A token carries a tag drawn once per boot, so a debt written after a restart can never take the token of a row still owed from before.
  • Message order. The buyer reads You bought … first, then, if the car was not handed over, where it waits.
  • A player who is down cannot buy, offer or accept an offer (error.incapacitated); a no still goes through. A refusal names back a stock key only when it is a row of the stock.

Showroom previews

Each PREVIEW.POINTS row stands one stock model, locked and persistent, at most PREVIEW.LIMIT per dealer. A point naming a missing dealer is skipped with a log line. A point whose ENTRY is not a stock key is reported at boot.

Legacy tables

  • /opx.dealership.add, /opx.dealership.remove and the preview placement path (with its ACL right opx.dealership.place) no longer exist.
  • At every boot the server reads opx77_dealerships and opx77_dealership_previews, adopts every row whose key is not in the config (config wins), and logs each as [dealership] config line: ..., ready to paste into SPOTS or PREVIEW.POINTS. Nothing writes to these tables any more.
  • A role that still holds opx.dealership.place holds a right nothing checks.

Configuration

config/dealership.lua sets OPX.Config.MODULES.dealership. Shared.

KeyDefaultEffect
enabledtruefalse switches the module off.
CURRENCY'EDDIES'Money type prices are in. An unknown type refuses every sale (boot error).
HAND_OVERtrueCreate the bought car on the dealer marker. false only files it.
USE_RADIUS4.0Flat metres from a dealer within which it can be used. The server allows REACH_SLACK (1 m) more.
MARKERgarage = cylinder/interaction/2.5, avpad = ring/interaction/3.5Marker look per kind: { shape, style, RADIUS }.
MAX_DISTANCE150.0Metres beyond which a marker is not drawn (1–500).
GROUND_OFFSET0.06Marker lift off Z (0–2).
AV_LIFT1.2Metres above the dealer an AV is handed over (0–10).
SCAN_MS500Client marker and zone scan interval.
POLL_MS15000How often the client re-asks for dealers.
REQUEST_WINDOW_MS10000Per-player request window.
REQUESTS_PER_WINDOW6Requests allowed per window (buy, offer, decide).
COOLDOWN_MS3000Minimum gap between two purchases.
KEY{ ID = 'opx.dealership.use', NAME = 'dealership.key.use', DEFAULT = 'E' }Key mapping, separate from the garages key. DEFAULT = false registers none.
MENU{ ANCHOR = 'center', WIDTH = 708, HEIGHT = 708, MAX_HEIGHT_VH = 88, VISIBLE_ROWS = 15 }List panel size; clamped by the menu module.
COMMANDS{ list = 'opx.dealership.list', stock = 'opx.dealership.stock', buy = 'opx.dealership.buy' }Command names.
PREVIEW{ ENABLED = true, LIMIT = 12, LOCKED = true, LIFT = 0.1, POINTS = {} }Showroom cars. LIMIT per dealer; LIFT metres added to Z (0–2). POINTS below.
ZONE_RADIUS30.0Flat metres around a dealer where the "sell a vehicle" row appears. Must be ≥ USE_RADIUS.
OFFER_TIMEOUT_MS30000How long a buyer has to answer an offer.
SELLER_CUT_PERCENT10Seller's commission, 0–100, rounded down. The rest goes to the company.
COMPANY{ JOBS = true, GANGS = true, EXCLUDED = { unemployed = true, none = true } }Which groups have a company account. Both off: nobody can sell to another player.
STOCK30 rows in classes Street, Sport, Utility, Bikes, AirWhat is sold. Below.
SPOTSgarage1 (one garage dealer)Every dealer. Below.
config/dealership.lua — one STOCK row
{ KEY = 'hella', LABEL = 'Archer Hella', CLASS = 'Street',
  RECORD = 'Vehicle.v_standard2_archer_hella_player', PRICE = 29000 },
  • KEY: 1–48 characters, unique. LABEL: 1–64. CLASS: 1–32.
  • RECORD: a TweakDB record (≤ 256). Whether the row is an AV comes from RECORD and AV_PREFIXES in config/shared.lua.
  • PRICE: a whole number above zero.
  • A bad row is skipped with a boot warning.
config/dealership.lua — a dealer and a preview point
SPOTS = {
    watson_autos = { LABEL = 'WATSON AUTOS', KIND = 'garage',
        X = -1771.79, Y = -77.30, Z = 7.53, HEADING = 90.0, BUCKET = 0 },
},
PREVIEW = { POINTS = {
    show_hella_1 = { X = -1536.1, Y = -205.9, Z = 7.86, HEADING = 52.0,
        BUCKET = 0, DEALER = 'watson_autos', ENTRY = 'hella' },
} },
  • Both follow the shared spot rules: key 1–48 characters; X, Y, Z finite within ±1,000,000; HEADING finite (default 0); BUCKET whole ≥ 0 (default 0); LABEL defaults to the key. Only X and Y are measured.
  • A dealer needs KIND garage or avpad.
  • A preview needs DEALER (a key in SPOTS) and ENTRY (a key in STOCK).
  • To capture a position and heading, stand there facing the right way and run /opx.admin.self.pos.

Refusal codes

Each code is also a locale key. Codes from vehicles.Register (e.g. vehicle.limit) pass through after a refund.

CodeMeaning
dealership.noVehiclesNo vehicles contract.
dealership.noCharacterNo character loaded (or it changed before the answer).
dealership.noPositionPosition could not be read.
dealership.noSuchSpotNo dealer underfoot or by that key.
dealership.wrongBucketThe dealer is in another routing bucket.
dealership.tooFarFurther than USE_RADIUS.
dealership.noSuchEntryNo stock row by that key (or its price changed since the offer).
dealership.notSoldThis dealer's kind does not sell that row.
dealership.noCurrencyCURRENCY is not a money type.
dealership.cannotAffordNot enough money.
dealership.noSuchGarageThe chosen garage (counter or offer) is not one of this kind in the buyer's bucket.
dealership.paymentFailedThe charge was refused (or the character module's own code).
dealership.registerFailedvehicles.Register failed unexpectedly; the price was refunded.
dealership.noCompanyThe seller has no job or gang with an account.
dealership.noSuchBuyerBad buyer id, or the seller targeted themself.
dealership.notInZone / dealership.buyerNotInZoneSeller or buyer outside ZONE_RADIUS. Offer answers buyerNotInZone for any id outside the showroom, connected or not, before reading the buyer's character.
dealership.buyerCannotAffordNo longer answered. Since PR #93 the offer always goes out; on accept the buyer reads dealership.cannotAfford and the seller only dealership.paymentFailed, so offers at rising prices reveal nothing about a stranger's balance.
dealership.noOfferNo offer waiting, or the token does not match.
error.incapacitatedThe player is down: no buy, offer or yes from the floor.
dealership.offerDeclined / dealership.offerExpiredThe buyer said no, or did not answer in time.
dealership.sellerGone / dealership.buyerGoneThe other side left or walked out of the zone.
dealership.sellerNoCompanyAt accept, the seller no longer sells for that company.
dealership.handOverBlockedNot a refusal: every hand-over spot was taken, the car is in its garage.
dealership.nothingForSale / dealership.noListClient only: empty stock, or the menu could not open.
error.noPermissionClient only: another surface holds the keyboard.
error.tooFast, error.badRequestCooldown or request window; malformed request.

On this page