Pro Engineer/RGE
Small enough to read in one sitting. Enough to build something with.
Every telemetry program worth the name lets you make a channel the game does not publish. RGE started there and went further: arithmetic over the numbers already coming out of the simulator, then words with the car written into them, things a formula does as well as works out, something kept between frames, and a canvas you can draw a rev counter or a map of the track on. A screen you wrote is a tab of the program.
It is total, and that is the whole design. No loops, no recursion, nothing that allocates while it runs, every jump forward. A formula cannot be written badly enough to cost a frame, however long the document gets — which is what lets it run inside a window you are racing beside.
What a formula can read. Each is one number at one instant, taken from the lap the program already has — there is nothing to import and nothing to name.
| THE LAP | |
distance (0-1) | How far round the lap, nought at the line. |
time (s) | Seconds since the lap started. |
x (m) | Position across the map. |
y (m) | Position along the map. |
| THE CAR | |
speed (km/h) | Road speed. |
gear | Gear. Neutral is nought, reverse is negative. |
rpm (rpm) | Engine speed. |
fuel (L) | Fuel in the tank. |
brake_bias (%) | How much of the braking is at the front. |
| WHAT YOU ARE DOING | |
throttle (0-1) | Throttle pedal, nought to one. |
brake (0-1) | Brake pedal, nought to one. |
steer (-1..1) | Steering, negative to the left. |
clutch (0-1) | Clutch pedal. |
| FORCES | |
lat_g (g) | Cornering load. Positive to one side. |
lon_g (g) | Braking and acceleration. Negative under brakes. |
vertical_g (g) | Up and down: kerbs, crests, compressions. |
| TYRES | |
slip | How much the tyres are sliding, all four averaged. |
tyre_temp_fl (°C) | Tyre core temperature, front left. |
tyre_temp_fr (°C) | Tyre core temperature, front right. |
tyre_temp_rl (°C) | Tyre core temperature, rear left. |
tyre_temp_rr (°C) | Tyre core temperature, rear right. |
tyre_inner_fl (°C) | Tread temperature at the inner edge, front left. |
tyre_inner_fr (°C) | Tread temperature at the inner edge, front right. |
tyre_inner_rl (°C) | Tread temperature at the inner edge, rear left. |
tyre_inner_rr (°C) | Tread temperature at the inner edge, rear right. |
tyre_middle_fl (°C) | Tread temperature across the middle, front left. |
tyre_middle_fr (°C) | Tread temperature across the middle, front right. |
tyre_middle_rl (°C) | Tread temperature across the middle, rear left. |
tyre_middle_rr (°C) | Tread temperature across the middle, rear right. |
tyre_outer_fl (°C) | Tread temperature at the outer edge, front left. |
tyre_outer_fr (°C) | Tread temperature at the outer edge, front right. |
tyre_outer_rl (°C) | Tread temperature at the outer edge, rear left. |
tyre_outer_rr (°C) | Tread temperature at the outer edge, rear right. |
pressure_fl (psi) | Tyre pressure, front left. |
pressure_fr (psi) | Tyre pressure, front right. |
pressure_rl (psi) | Tyre pressure, rear left. |
pressure_rr (psi) | Tyre pressure, rear right. |
wear_fl (%) | How much tread is left, front left. 100 is new. |
wear_fr (%) | How much tread is left, front right. 100 is new. |
wear_rl (%) | How much tread is left, rear left. 100 is new. |
wear_rr (%) | How much tread is left, rear right. 100 is new. |
grip_fl | How much that tyre is sliding, front left. |
grip_fr | How much that tyre is sliding, front right. |
grip_rl | How much that tyre is sliding, rear left. |
grip_rr | How much that tyre is sliding, rear right. |
| BRAKES | |
brake_temp_fl (°C) | Brake disc temperature, front left. |
brake_temp_fr (°C) | Brake disc temperature, front right. |
brake_temp_rl (°C) | Brake disc temperature, rear left. |
brake_temp_rr (°C) | Brake disc temperature, rear right. |
brake_pad_fl (mm) | Brake pad left, front left. |
brake_pad_fr (mm) | Brake pad left, front right. |
brake_pad_rl (mm) | Brake pad left, rear left. |
brake_pad_rr (mm) | Brake pad left, rear right. |
| SUSPENSION | |
ride_front (mm) | Ride height at the front. |
ride_rear (mm) | Ride height at the rear. |
load_fl (N) | How hard that corner is being pressed into the road, front left. |
load_fr (N) | How hard that corner is being pressed into the road, front right. |
load_rl (N) | How hard that corner is being pressed into the road, rear left. |
load_rr (N) | How hard that corner is being pressed into the road, rear right. |
travel_fl (m) | How far the suspension has moved, front left. |
travel_fr (m) | How far the suspension has moved, front right. |
travel_rl (m) | How far the suspension has moved, rear left. |
travel_rr (m) | How far the suspension has moved, rear right. |
camber_fl (°) | Camber, front left. Negative leans the top of the tyre in. |
camber_fr (°) | Camber, front right. Negative leans the top of the tyre in. |
camber_rl (°) | Camber, rear left. Negative leans the top of the tyre in. |
camber_rr (°) | Camber, rear right. Negative leans the top of the tyre in. |
| AIDS | |
tc (0-1) | How hard traction control is working. |
abs (0-1) | How hard the anti-lock is working. |
ffb (0-1) | Force feedback on the wheel. |
| CONDITIONS | |
air_temp (°C) | Air temperature. |
road_temp (°C) | Road temperature. |
23 take numbers and give a number, trigonometry included —
and every angle is in degrees, in and out, because every angle this program
shows is. The last three ask the whole lap rather than this instant —
speed / highest(speed) is where you are against the best you managed
— and they are worked out once when the lap changes, never per sample.
abs(a) | Drop the sign. |
sqrt(a) | Square root. |
sin(a) | Sine of an angle in degrees. |
cos(a) | Cosine of an angle in degrees. |
tan(a) | Tangent of an angle in degrees. |
asin(a) | The angle, in degrees, whose sine this is. |
acos(a) | The angle, in degrees, whose cosine this is. |
atan(a) | The angle, in degrees, whose tangent this is. |
atan2(a, b) | The direction of (x across, y along), -180 to 180 degrees. |
log(a) | Natural logarithm. |
log10(a) | Logarithm base ten. |
exp(a) | e raised to this. |
floor(a) | Down to the whole number below. |
ceil(a) | Up to the whole number above. |
round(a) | To the nearest whole number. |
sign(a) | -1, 0 or 1, by which way it points. |
min(a, b) | The smaller of the two. |
max(a, b) | The larger of the two. |
hypot(a, b) | The two as sides of a triangle: combined g. |
clamp(a, b, c) | Hold a value between a floor and a ceiling. |
lerp(a, b, c) | Blend from the first to the second, by 0 to 1. |
highest(a) | The largest it got anywhere on the lap. |
lowest(a) | The smallest it got anywhere on the lap. |
average(a) | Its average over the whole lap. |
best(a) | What it was here on your quickest lap. `speed - best(speed)` is the delta. |
last(a) | What it was here on the lap you just finished. |
The four things a formula can do rather than work out. Each takes a piece of
text with the same {…} holes a sentence has, and each is worth 1 if it
happened. Only the arm of an if that is taken runs, so
if a then print "yes" else print "no" says one of them and not both
— and a row that fired warn or alert draws itself in
that colour, which is one thought written once.
print "…" | Write a line under the row. |
warn "…" | The same, in the warning colour. |
alert "…" | The same, in the alarm colour. |
title "…" | Rename the tab this row is on. |
The whole of a formula. There are no loops, no recursion and no way to call anything you wrote, and every jump goes forward — which is what makes a formula impossible to write badly enough to cost a frame.
+ - * / % | The arithmetic, in the usual order. |
^ | Power. Right to left, so 2 ^ 3 ^ 2 is 512. |
( ) | Brackets, up to thirty-two deep. |
< <= > >= == != | Comparisons. They give 1 or 0. |
and or not | Logic, in words rather than symbols. |
if … then … else … | A choice. Both sides are worked out, so neither can be slow. |
? : | The same choice, written short: speed > 100 ? 1 : 0. |
Install RGE script studio from the MODULES tab. A SCRIPT tab appears. Type a formula into the scratchpad; it is checked as you type, coloured as you type, the number it gives right now is under it, and its shape over your last lap is under that. Every channel on this page is listed there too, with your car’s current value beside it — type three letters and the rest of the name is offered.
When it does what you meant, KEEP IT AS gives it a name and it lands in CHANNELS: plotted, kept between sessions, recomputed on the lap you are driving.
# combined g, which no simulator publishes hypot(lat_g, lon_g) # how much of the rev range a lap actually used rpm / 8000 * 100 # speed, but only while the brake is down — two ways to write it if brake > 0.05 then speed else 0 brake > 0.05 ? speed : 0 # slip as a percentage, never past 100 clamp(slip * 100, 0, 100)
A formula that cannot produce a number at a sample — a division by a channel that was zero — leaves that sample out rather than plotting a nought. That rule runs through the whole program: a gap is a gap and a zero is a claim about the car.
Your own screen is a document rather than a program. One line per thing you
want to see, and nothing has to line up — KEEP IT lays it out for you. It draws
under the editor in STUDIO → SCREENS as you type; write screen NAME
and it becomes a tab of its own as well.
# a name for a piece of maths, used by the rows below let g = hypot(lat_g, lon_g) columns 2 panel HOW HARD IT IS WORKING Combined g (g) = g Grip used (%) 0..100 = clamp(g / 1.6 * 100, 0, 100) ~ Speed (km/h) = speed panel IN WORDS "Doing {speed:0} km/h in gear {gear:0}." "Fuel is low — {fuel:1} L left." when fuel < 5
Name (unit) = formula | A number, read at a glance from a metre away. |
Name (unit) low..high = formula | A range makes it a bar — a bar is the thing that needs two ends. |
~ Name (unit) = formula | A line over the lap. |
"Words with {a formula} in them." | A sentence. `{fuel:1}` is one decimal place; two is the default. |
"…" when formula | Only shown while that reads as true — which is how a warning is written. Sentences only: a reading that vanished could not be told from one you deleted. |
panel HEADING | Starts a box. The rows after it go in it. |
paint HEADING 200 | Starts a canvas, that many points tall. What goes on it is marks. |
dot x, y · dot x, y, size | On a canvas: a point. Every number is a formula. |
line x, y, x, y | On a canvas: a straight piece. |
box x, y, width, height | On a canvas: a rectangle. |
ring x, y, radius | On a canvas: a circle, drawn round. |
arc x, y, radius, from, to | On a canvas: part of one. Degrees, 0 straight up, going clockwise. |
text x, y "…" | On a canvas: words, with the same holes a sentence has. |
here across, along | On a canvas: a point in the space the `path` above it drew — `here x, y` is where the car is on the map. |
path across, along | On a canvas: the whole lap as a shape — `path x, y` is a map of the track you are on. Fitted to the canvas, so the numbers need no scaling. |
… in accent | Any mark, in a named colour: accent, highlight, good, warning, alarm, text, dim. |
… when formula | Any mark, only while that is true — which is how a warning light is drawn. |
screen NAME | Starts a tab. A document with no `screen` in it draws under the editor and nowhere else. |
instead of DASHBOARD | Starts a tab standing where one of the program's own was. |
instead of ANALYSIS / CORNERS | The same for one sub-tab. The others stay the program's own, and the strip above stays where it was. |
columns 2 | How many panels sit side by side, 1 to 4. |
let name = formula | Names a piece of maths for the rows below. |
state name = 0 | Declares something kept between frames, and the number it starts at. |
remember name = formula | Writes to it, once a frame, in the order written. |
module "Name" · about "…" | What the whole document calls itself. It appears on MODULES beside the program’s own. |
# anything | A note to yourself. |
It is drawn by the same widgets every other screen uses and costs the same, because the layout is yours and the drawing is still the program’s. A line that does not read costs its own row and names its line number — half a document still draws, because a screen that went blank on a typo is a screen nobody would dare change. KEEP IT saves; BACK returns to the last version you kept.
A formula is an expression and always will be — that is what makes it
impossible to write one that costs a frame. What an expression cannot do is
remember: a fastest lap, a counter, how long something has been true.
state declares a slot and the number it starts at;
remember writes to it, once a frame, in the order written, so a later
line sees what an earlier one just wrote.
state best = 0 state seen = 0 remember best = max(best, speed) remember seen = seen + (speed > 5 ? 1 : 0) panel HOW IT IS GOING Best (km/h) = best Off the best (km/h) = best - speed "You have been moving for about {seen / 60:0} seconds."
A slot is read anywhere a channel is. It is not saved: it is what is happening in this session, and restoring yesterday’s peak into today’s would be a number nobody measured — which is this program’s worst class of bug. A formula that has no answer for one frame leaves the slot alone rather than wiping it.
Per lap rather than per session needs no keyword of its own.
time is seconds since the lap started, so it goes back to nought at the
line, and that is the whole of it:
state this_lap = 0 remember this_lap = time < 1 ? speed : max(this_lap, speed)
The lines run in the order they are written, once a frame, wherever you are looking — a fastest lap does not stop counting because you opened another tab.
best(f) is what f was at this place on the track on
your quickest complete lap of the session; last(f) is the same on the
lap you just finished. So the thing every telemetry program is for is one line:
let delta = speed - best(speed) panel HERE, AGAINST YOUR BEST Now (km/h) = speed Your best had (km/h) = best(speed) Difference (km/h) = if delta < -0.5 then alert "losing it here" else delta # and the two traces, plotted against each other ~ This lap (km/h) = speed ~ Your best (km/h) = best(speed) ~ Difference = delta
They are a table by distance, not a number — that is what makes
speed - best(speed) mean “slower than your best lap
here” rather than “than its average”. The table is worked
out once when the lap changes, so a formula asking it costs one lookup per sample.
Inside best(…) everything is about that lap:
best(highest(speed)) is the highest it went round the quickest lap.
Until there is such a lap they give nothing, and a row reading nothing draws a dash. A delta of zero in your first minute would read as “exactly on your best”, which is the one thing it must not say.
paint NAME 220 is a panel you draw on. The space is 0 to 1 across and
0 to 1 down, top-left to bottom-right, and a radius is a fraction of whichever
side is shorter — so a ring is round on a canvas of any shape. Angles are
degrees, 0 straight up, going clockwise, because that is how a dial is read.
Every number is a formula.
let turn = rpm / 8000 paint REVS 220 ring 0.5, 0.55, 0.38 in dim arc 0.5, 0.55, 0.38, -130, 130 in dim arc 0.5, 0.55, 0.38, -130, -130 + turn * 260 in accent arc 0.5, 0.55, 0.38, 110, 130 in alarm line 0.5, 0.55, 0.5 + sin(-130 + turn * 260) * 0.3, 0.55 - cos(-130 + turn * 260) * 0.3 text 0.5, 0.55 "{rpm:0}"
Nine lines and it is a rev counter with a needle, a red zone and the number in the
middle. It costs one rectangle and one loop: a mark is a line segment, and giving
each one an id, a response and a layout would cost more than drawing it. Any mark
takes the same when a sentence does, so a warning light is one more
line.
Two marks are about the whole lap rather than this instant. path x, y
is a map of the track you are on — it is fitted to the canvas, at one
scale on both axes so the circuit keeps its shape, which is why the numbers need no
scaling of yours. here x, y puts a point in that same space.
paint THE TRACK YOU ARE ON 220 path x, y in dim here x, y, 0.025 in accent
A document that says module "Fuel Warden" has a name, a line about what
it is for, and tabs of its own — every part of what a module is from where a
driver sits, minus the part where somebody else compiled it. It appears on the
MODULES screen beside the ones that shipped, and it travels as one line like
everything else here.
module "Fuel Warden" about "Tells me when to box, and keeps my best lap." screen FUEL
The in-game panel is Lua because Custom Shaders Patch is. Here it would mean an interpreter, a garbage collector and a foreign stack between the driver and every frame, for a job whose whole grammar fits on one screen. The cost of RGE is that it is ours to maintain; the cost of the alternative is a collector running during a braking zone.
It is also why RGE is total: there is no way to write a formula that does not finish. The worst a bad one can do is be wrong, and a wrong number is caught by the same rule as everything else here — if it is not a finite number, it is not drawn. It is the same reason a screen is a description and not a program: writing the interface in the language would put an interpreter inside the drawing loop, which is the cost this refused in the first place.