Simulator software integration
Who this is for: whoever decides what goes in your next release, and whoever writes it.
You are the only participant in the bay that knows where the ball is. Everything else in the room measures something — speed, spin, a swing. You know the hole, the lie, the distance left, and the score. That is why the standard needs you more than it needs any launch monitor, and it is why most of what follows is short: you already have this data. It is a matter of handing it over in a shape everyone agrees on.
This guide goes feature by feature. For each one: what it is, why you would want it, what it costs you, and what happens if you skip it. Almost all of them are optional. Skipping one degrades something specific rather than breaking the integration, and this document tries to be honest about exactly what.
Nothing here asks you to expose anything proprietary. No physics, no rendering, no course data beyond a hash, no source code.
The short version#
| # | Feature | What your users get | Effort |
|---|---|---|---|
| 1 | Nobody types names | Scan a code, the player list fills itself | Small |
| 2 | The code on your screen | They check in without walking to the wall | Small |
| 3 | Matches that set themselves up | Net scoring with no arguing at the tee | Small |
| 4 | Five fields per shot | Rounds that actually count toward a player rating | Small |
| 5 | Every stroke | Scores that reconcile instead of drifting | Small |
| 6 | Scorecard checkpoints | A round that survives a crash | Medium |
| 7 | Name the course | Two bays can be compared honestly | Small |
| 8 | Settings aren't weather | Their own stats stay honest | Tiny |
| 8a | Say what is switched on | Rounds count fully toward a player rating — and you are most of the way to tournaments | Medium — the big one |
| 9 | Declare your features | The engine stops offering what you can't do | Tiny |
| 10 | Accept a rules package | Your platform can host real tournaments | Medium |
| 11 | Your declaration is the report | ” | Small, once 8a exists |
| 12 | Changes are new declarations | ” | None — 8a does it |
| 13 | Lock out fake input | ” | Medium |
| 14 | Transport | Shots that never lag | Small |
| 15 | Ship the bundle | Bays that stay current on their own | Small |
| 16 | How far your rounds count | From A1 up, rounds count toward a player rating | Small to start |
Effort is relative and describes what has to change in your code, not how long it takes your team. "Small" means reading a field and using it. "Medium" means new behavior. There is exactly one "big one", and it is #8a — which is also what makes a round count fully toward a player rating, so it is not tournament-only work.
The minimum, in two frames#
// Once, after session.hello: who you are, what you provide, what you want.
{ "type": "session.describe", "protocol_version": "1.0",
"identity": { "device_id": "sga:device:sim-1", "vendor": "acme", "model": "Acme Sim",
"serial": "SIM-0001", "firmware": "1.0", "attestation_tier": "A0" },
"roles": ["simulator"],
"provides": [
{ "field": "round.roster", "provenance": "inferred", "model": { "id": "acme.round", "version": "1" },
"timing": "standing", "max_age_ms": 600000 },
{ "field": "round.player_up", "provenance": "inferred", "model": { "id": "acme.round", "version": "1" },
"timing": "standing", "max_age_ms": 600000 },
{ "field": "round.hole", "provenance": "inferred", "model": { "id": "acme.course", "version": "1" },
"timing": "standing", "max_age_ms": 600000 },
{ "field": "status.sim_ready", "provenance": "inferred", "model": { "id": "acme.round", "version": "1" },
"timing": "standing", "max_age_ms": 60000 },
{ "field": "origin.lie_surface", "provenance": "inferred", "model": { "id": "acme.course", "version": "1" },
"timing": "standing", "max_age_ms": 600000 },
{ "field": "origin.distance_to_pin_m", "provenance": "inferred", "model": { "id": "acme.course", "version": "1" },
"timing": "standing", "max_age_ms": 600000 },
{ "field": "outcome.end_lie_surface", "provenance": "calculated",
"from": ["ball.speed_mps", "ball.launch_angle_deg", "ball.launch_direction_deg", "ball.spin_rpm"] },
{ "field": "outcome.end_distance_to_pin_m", "provenance": "calculated",
"from": ["ball.speed_mps", "ball.launch_angle_deg", "ball.launch_direction_deg", "ball.spin_rpm"] }
],
"wants": [ { "field": "ball.speed_mps" }, { "field": "ball.launch_angle_deg" },
{ "field": "ball.launch_direction_deg" }, { "field": "ball.spin_rpm" },
{ "field": "bay.checked_in" } ],
"features": { "round_resume": false, "settings_attestation": true, "scorecard_checkpoints": true } }
// After each stroke: where the ball went. The shot_id comes from record.swing_detected.
{ "type": "record.shot", "field": "outcome.end_distance_to_pin_m", "shot_id": "<from the engine>",
"hole": 4, "stroke_index": 2,
"outcome": { "end_lie_surface": "green", "end_distance_to_pin_m": 3.2 } }Nothing you provide is measured: a modeled world is not an observation. Where the ball finished is calculated from the launch data you were sent; the state of your own game is inferred from your course and round models. There is one simulator per bay, and a second is refused with role_occupied. Publish round.roster and round.player_up as the round is set up and played: check-in claims a slot from the roster (§1), and the player up decides whose stroke each shot becomes.
Part 1 — The wins#
These three cost the least and are the ones your users will actually notice. If you only ever build three things from this document, build these.
1. Nobody types names again#
What it is#
Right now, somebody stands at your touchscreen and types four names with a trackball. Those names are guesses — "Sam", "sam r", "SAM R." — and nothing downstream can do anything with them.
Instead: a golfer scans the bay's code with their phone. The engine holds onto them, along with the screen name from their SGA account. By the time the group opens your player-configuration screen, the list is already sitting there waiting to be offered.
{ "bay.checked_in": [
{ "checkin_ref": "9c1f4a2e", "display_label": "Sam R",
"checked_in_at": "2026-02-14T18:02:11Z", "expires_at": "2026-02-14T22:02:11Z" },
{ "checkin_ref": "51b0d7c3", "display_label": "Andrea",
"checked_in_at": "2026-02-14T18:04:47Z", "expires_at": "2026-02-14T22:04:47Z" } ] }Why you want it#
Setup is the least enjoyable minute of a session, and it is the first one. You replace typing with tapping a name that is already correct.
And you get it without building an account system. No sign-up, no password reset, no email verification, no breach to disclose. The golfer already has an SGA account; you are handed a label to show and an opaque reference to hand back. You never hold a credential.
What it costs you#
You read a list and pre-fill the player picker you already have.
If you skip it#
Names get typed. The shots still record, but they land in unclaimed slots — the round exists, nobody owns it, and no player rating comes out the other end. Which is the thing the golfer turned up for.
The two rules that matter#
- Let the operator edit or remove anyone you pre-fill. A scan means a phone was at the bay. It does not mean that person is playing this round. Somebody scans, gets a coffee, and watches — do not put them in the group.
- When you put a checked-in person in a slot, record their
checkin_refagainst that slot. This is the whole point. That one string is what lets the cloud match the round to a member later, with nobody typing a name or scanning a second time. Drop it and you have done the work and thrown away the result.
checkin_ref is opaque. It is not a member ID, it is not derived from one, and neither you nor the bay can turn it into one. It means "the person who scanned in at 18:02" and only the cloud can say who that was. Entries expire — honor expires_at and stop offering someone who left hours ago.
Full detail: spec/03 §3.4a.
2. Put the code on your own screen#
What it is#
Check-in assumes a printed placard on the wall. But you have a large screen that everybody is already looking at, and your player-selection screen is the exact moment somebody wants to check in. So ask the engine for the code and draw it.
{ "bay.checkin_code": {
"payload": "https://golfsga.com/c/8F3K2Q",
"short_code": "8F3K2Q",
"expires_at": "2026-02-14T18:12:00Z",
"rotates": true } }Render payload as a QR code. Show short_code underneath for the person whose camera will not focus.
Why you want it#
The obvious reason is that nobody walks to the wall.
The better reason: a printed placard is ink. It says "this is bay 12" and it says that forever. A code on a screen can carry a short-lived nonce, so a scan proves somebody was at this bay in this ten-minute window rather than at some point since the sign was printed.
That matters because presence is what makes a round rankable. By drawing this, your platform produces a materially stronger presence signal than the placard it replaces — which is a real reason for a facility to prefer you when they are deciding what to install.
What it costs you#
A QR library and a panel on a screen you already draw.
If you skip it#
Nothing breaks. The placard is still on the wall.
Rules#
- It arrives in
MANIFEST, never inHELLO.HELLOhappens before anyone has proved where they are, and this code is what proves somebody was physically at the bay — hand it to an unauthenticated peer and somebody checks in from the car park. - Display only. Do not log it, do not store it past display, do not forward it anywhere.
- Stop drawing it once
expires_atpasses. Ask for a fresh one.
Full detail: spec/03 §3.4c.
3. Rated matches that set themselves up#
What it is#
Four people want a match. Two are good, two are not. Today somebody works out strokes on their phone, or they argue, or they skip it and play a match nobody enjoys.
If every player in the group has an SGA player rating, the engine hands you the strokes already worked out.
{ "round.allowances": { "complete": true, "source": "sga", "players": [ … ] } }Why you want it#
Net match play is a feature your users ask for and almost nobody ships well, because doing it properly means knowing everyone's player rating — which you have no way to know. Now you do.
What it costs you#
Read the field, apply the strokes, show both numbers.
The two ways to get this wrong#
1. All players, or none. Check complete before you apply anything.#
complete is true only when every occupied slot has an allowance. One unrated player and it is false.
complete == true -> you may pre-apply, operator may still override complete == false -> behave exactly as if the field were absent field absent -> behave exactly as you do today
A false means apply nothing at all — not "apply what you have and treat the rest as scratch." An unrated player is not a scratch player. Treating them as one hands the match to whoever the association happens not to know yet, which is precisely backwards.
2. Keep the real strokes. Report gross and net separately.#
An allowance changes exactly one number: the net result. It changes nothing about what was played.
gross is what the player actually shot and it never moves. A player who shot 84 with five strokes received reports gross: 84 and net: 79. Both. In the same slot object. Never 79 in the gross field.
The player rating posts from gross. Net written into the gross field compounds every round: each allowance is computed from an already-adjusted score.
Your existing reconciliation is untouched and must stay exact: hole.strokes == sum of counted records. Allowances are not strokes and never appear in that sum.
In a sanctioned event the allowance comes from the Event Rules Package with source: "event", and it wins over everything else — a committee cannot audit a number that was fetched three minutes before the first tee shot.
Full detail: spec/03 §3.4b.
3a. Let the bay follow the player#
What it is#
Two standing booleans and one standing value, all published by you, all cheap:
| field | you say |
|---|---|
status.sim_ready | the round can accept a stroke right now |
player.sim_handedness | which hand the software is set up for |
round.hole | which hole is in play, with its par and length |
And two you subscribe to, published by the launch monitor: status.lm_ready and player.lm_handedness.
Why you want it#
Handedness is the one that saves a round. A simulator configured for a right-hander while a left-hander plays mirrors the shot, and every number after that is wrong in a way nobody notices for eighteen holes. A launch monitor that arms on one side of the ball already knows which hand is being used. Subscribe to player.lm_handedness, correct your setup, and the bay follows the player with nobody touching a menu.
Publish your own player.sim_handedness alongside it and the monitor can flag the mismatch the other way, before the round is scored (why two fields).
Readiness saves the shots at the edges. The commonest way a real shot goes unrecorded is that it was struck while one half of the bay was not looking — you were mid-flyover, or the monitor was recalibrating. Publishing status.sim_ready and subscribing to status.lm_ready lets each side prepare: a monitor that sleeps between shots knows when to wake, a capture rig knows when to arm its buffer, and you can hold the tee until the monitor can see.
Nothing forces you to act on it. There is deliberately no interlock and the engine composes nothing from the two values (03 §3.4e) — they are published, journaled, and yours to use.
round.hole is what makes a score mean anything. The engine spreads it across every shot record it composes: hole number, par, length, course name. A round played without it produces shots that cannot say what hole they were on, and a score is a number of strokes until something says what par was. You are the only thing in the room that knows.
What it costs you#
Four standing assertions you already have the values for, and two subscriptions.
If you skip it#
Handedness stays a menu somebody has to remember, and the mismatch is found after the round instead of during it. The shot records carry no hole context, so the platform can count strokes but cannot score them.
Declare it, then send it#
If it is in provides, something in your code must send it. A declared field that never arrives leaves every subscriber waiting with no warning, and it is recorded at the end of the round (details).
Part 2 — The record#
This is what turns "I played golf indoors" into "this round counts."
4. Five fields make a shot count#
What it is#
| field | what it means |
|---|---|
origin.lie_surface | what the ball was sitting on |
origin.distance_to_pin_m | how far it had to go |
outcome.end_lie_surface | what it ended up on |
outcome.end_distance_to_pin_m | how far is left |
hole, stroke_index | which stroke this is |
Why you want it#
Those five make a shot ratable with no launch monitor data at all.
A player rating is computed from where the ball started and where it stopped — not from ball speed. So a bay with nothing but your software can still produce rounds that count. You are the load-bearing participant for the thing SGA exists to do, and these five fields are the whole of it.
What it costs you#
You already have all five on screen. This is a serialization job.
If you skip it#
The round is a story, not a record. Nothing rates.
Strongly wanted, one step up#
origin.elevation_change_m— a 100 m shot playing 15 m downhill and one playing 15 m uphill are different shots. Without it, a rating model is measurably wrong.origin.lie_slope_along_deg/lie_slope_across_deg
5. Report every stroke, including the ones with no swing#
What it is#
Penalties. Gimmes. Concessions. Drops. Every stroke that counts against the score, whether or not anybody swung a club.
Why you want it#
Because the arithmetic has to close:
hole.strokes == sum of counted records
If a player takes a penalty drop and you record the score but not the stroke, the hole says 6 and the records add to 5. Now nothing downstream can tell whether a shot was lost in transit or never existed. Every round from your platform gets flagged as suspect, including the honest ones.
What it costs you#
You already count these — you have to, or your own scorecard would be wrong. This is emitting a record alongside the number.
If you skip it#
Your rounds reconcile badly and get treated as unreliable as a class. This is the single cheapest way to make your platform look untrustworthy when it isn't.
6. Scorecard checkpoints#
What it is#
At the end of each hole, send record.scorecard with reason hole_complete: the card so far, chained to the previous one by previous_checkpoint_hash.
Why you want it#
A round is twenty minutes of work per hole and people lose them to crashes, power cuts and a kid unplugging the PC. A checkpoint means the round is recoverable up to the last completed hole instead of gone.
The chaining also means nobody can quietly delete hole 14 later — if a hole's result vanishes, the chain says so.
What it costs you#
Writing a summary per hole and carrying the previous hash forward.
If you skip it#
An interrupted round is recorded as a partial. Not a disaster, but you lose the holes that were played.
6a. Say where the round begins and ends#
What it is#
Two records that wrap the round: round_started when play begins, naming the course, and round_ended when it stops, naming the card that closed it and why it stopped — completed, operator_ended, abandoned or session_closed.
In practice you get round_ended for free: a scorecard with reason final_score is the end of the round (06 §9.6). You do not send a separate message.
Why you want it#
Without a boundary, a session is an undifferentiated stream of strokes and nothing can say where one round stopped and the next began. Everything that matters downstream depends on knowing:
- The check-in codes expire. A final card retires the code that let people into that round, so the QR on your screen stops admitting players to a game that is over.
- The score posts as one round. A player rating system needs eighteen holes with a beginning and an end, not a day's worth of strokes from one bay.
- A partial round is still a round.
abandonedwith the strokes that were played is a real, postable record. Silence is not.
What it costs you#
Sending the final card you were going to send anyway, with reason set correctly. If you support a mid-round restart, send final_score before you reset rather than after — the card describes the round that just ended.
If you skip it#
Rounds run together. The bay keeps showing a live check-in code for a game nobody is playing, and the association receives strokes it cannot group.
7. Say which course this actually is#
What it is#
A course block with a digest — a hash of the course file. Or just let the engine compute it.
Why you want it#
A course name is not an identifier. There are half a dozen versions of most popular courses, with different tree lines, different green speeds and different hole lengths. Two players comparing scores on "the same course" may not have played the same course at all.
A digest settles it. It is also required for sanctioned events — you cannot run a tournament across a hundred bays without knowing they all loaded the same file.
What it costs you#
Hash a file you already loaded, or point the engine at it.
If you skip it#
Casual rounds still work. Tournament play does not.
8. Do not let a setting look like weather#
What it is#
One field:
"environment": { "conditions_source": "simulator_configured" }Why you want it#
If a player sets a 15 mph crosswind in your menu and that arrives looking identical to a real measured wind, then their stats are polluted forever. Every "how do I play into wind" answer is computed partly from wind somebody chose because it was fun.
Marking it costs you a constant string and keeps your users' own numbers honest.
If you skip it#
Configured conditions get mixed with measured ones and nobody can separate them again afterwards.
8a. Say what is switched on, every round#
The big one — and it pays twice.#
What it is#
One standing field, round.settings, published before the first stroke of every round and again whenever anything in it changes:
{
"assists": {
"complete": true,
"active": [
{ "id": "gimme", "effect": "skips_the_stroke",
"applies_to": ["putting"], "distance_m": 0.9 }
]
},
"conditions": { "mode": "fixed", "wind_speed_mps": 3.1, "green_speed_stimp": 11.5 },
"course": { "course_digest": "sha256:...", "tee_set": "Members" },
"swing_inputs": ["launch_monitor"],
"changed_by": "player"
}Every assist you offer is filed under one of six effects (03 §3.4g): it plays the stroke, skips it, changes where the ball goes, makes the course easier, gives the player another attempt, or only advises. The assists the standard names — auto_putt, gimme, mulligan, aim_assist and the rest — have their effect fixed. Anything of your own goes in your namespace, x-yourvendor.name, and you say what it does.
Why you want it#
Two reasons, and the same work serves both.
- Your players' rounds count. A player rating is built from shots the player actually hit. A round that says what was switched on lets the rating keep everything the assists didn't touch: a gimme costs the player nothing but the putt that was never struck. A round that says nothing can only be counted at a discount, because nobody can tell which shots were the software's.
- It is most of the way to hosting tournaments. An event compares exactly this declaration with its rules package (§11). Build it for every round and you have built the hard part of tournament eligibility.
The part that is easy to get wrong#
Report what is in force, not what was asked for. A player who sets green speed 11.0 in software that runs in half steps is playing on 11.5. Declare 11.5.
Say complete: true only when it is. It means "this is every assist switched on", and an empty list then means none. If you can't swear to that, say complete: false and list what you can — that is honest, and still worth a great deal. The one thing you must not do is leave an assist out of a complete list.
What it costs you#
Walking your settings menu once to classify each assist, and publishing the declaration whenever it changes. Medium, and only once.
If you skip it#
Every round your software hosts has no settings declaration: it counts toward player ratings at a discount, and its event verdict is unattested, so it cannot host a sanctioned event. (This verdict is separate from your A0–A3 trust level.)
9. Say what your software can actually do#
What it is#
"features": {
"round_resume": false,
"settings_attestation": true,
"course_hashing": true,
"scorecard_checkpoints": true,
"input_lockout": true,
"state_retraction": true,
"roster_add_in_round": false,
"roster_remove_in_round": false,
"operator_end_round": false
}Why you want it#
round_resume is the one to read twice. Can you save a round in progress and restore it in a later session? Many simulators cannot.
Saying false costs you nothing. The engine simply stops offering resumption it knows you cannot honor, and an interrupted round is recorded as a partial rather than held open pretending somebody can come back to it.
Declaring nothing is worse than declaring false, because then the engine has to guess — and it will guess wrong in the direction that makes your software look broken.
The roster and operator flags say what may be changed from outside your UI once a round is running (03 §3.4d). Absent means no.
What it costs you#
A few booleans.
9a. Answer requests, and know who may make them#
What it is#
The engine can ask you to do five things, and they all live in one namespace so that the complete list of what a bay can be asked is one thing to read:
| request | asks for |
|---|---|
request.scorecard | produce a card, for a stated reason |
request.add_player | a player joins — name, tee, check-in reference in any combination |
request.remove_player | a player leaves |
request.end_round | the round in progress ends |
request.apply_rules | put a signed event rules package in force (§10) |
Why you want it#
This is what makes a bay operable from somewhere other than the bay. A member who checked in on their phone appears in your player list because the cloud relayed request.add_player — they never touch your UI. A front desk ending an overrun booking sends request.end_round rather than walking over.
What it costs you#
Handling five message types, and honoring two rules:
asked_byis written by the engine, never by the asker. You can trust it.- A request carries a lifetime. Roster changes expire after two minutes. Apply a stale one and you have moved somebody who left ten minutes ago.
You may refuse any of them, and a refusal is a normal answer rather than an error. A bay in the middle of a tournament round has good reason to decline a roster change.
What you will never be asked#
There is no reboot, no arbitrary command, and no free-form arguments. Who may ask for each request is fixed by the standard (03 §1.1). Requests from a front desk or a member's phone arrive relayed by the cloud and marked asked_by.role: "operator"; no device in the bay can hold that role.
Part 3 — Tournaments#
The hardest part is #8a, which you build for every round anyway. Points 10–13 are what make a bay tournament-eligible, and they are the reason a facility can advertise your platform as competition-capable.
10. Accept a signed rules package#
What it is#
Before a sanctioned round, the engine hands you a signed Event Rules Package: the course digest, the conditions, the play rules, and the assists it prohibits or requires. You apply what it says.
The engine has already checked it. Before it sends request.apply_rules, the SGA Engine verifies SGA's signature on the package, that the package is for this event and this bundle version, and that it has not expired; it never delivers one that fails. You do not have to verify the signature yourself. You may, and if you do and it fails, refuse with signature_invalid (08 §1.1).
What you must do:
- accept
request.apply_rulesonly from the SGA Engine; - apply what you can;
- answer with
request.apply_rules_ack, accepted or refused; - report what is actually in force in
round.settings(§11); - for every setting the package pins, either limit changes to the values the package allows (preferred, §12a) or report every change as it happens, as a new version of
round.settings(§12).
Why you want it#
This is what makes an event an event. Every bay in the field plays the same course, the same wind, the same green speed, the same gimme rules — and can prove it.
What it costs you#
Mapping their keys to your settings, and applying them.
11. Your settings declaration is the report#
What it is#
Once you have accepted a rules package, publish your round.settings (§8a) with changed_by: "rules_package" and the package's request_id, showing what is actually in force. That is the whole report. There is no separate message for events: the engine compares the declaration you already publish in every round with the package, and signs the result into the journal.
engine you
│──── request.apply_rules (signed) ──────►│
│ │ apply what you can
│◄─── request.apply_rules_ack ────────────│ accepted or refused
│◄─── round.settings (rules_package) ─────│ what is actually in force
│ │
compare the package with the declaration, sign the resultSending settings to a bay does not make them true, so the engine compares the package with your declaration of what is live, not with the package itself.
The part that is easy to misread#
The acknowledgment says only whether you took the package up. What you put in force goes in round.settings.
Do not echo the package back. If the package asks for green speed 11.0 and your software only does half steps, declare 11.5. That is not a failure — it produces a clean divergent result a committee can read:
{ "rules_applied": {
"conforming": ["course.digest", "conditions.wind_speed_mps", "assists.aim_assist"],
"divergent": [
{ "key": "conditions.green_speed_stimp", "required": 11.0, "applied": 11.5 }
],
"unsupported": ["conditions.pin_positions"],
"verdict": "divergent" } }Declaring 11.0 while running 11.5 is the one outcome worth designing against. It reports conforming for a round that was not, which is the difference between a divergence a committee can rule on and a result that is quietly wrong with nobody aware of it.
A prohibited assist is only provably off in a complete declaration. If your assists.complete is false, an event cannot tell whether a prohibited assist was on, and every prohibited assist you did not list comes back unsupported.
If you skip it#
A simulator that publishes no round.settings is unattested, and unattested cannot host a sanctioned event. Everything else still works.
12. A mid-round change is a new declaration#
What it is#
Nothing new to build. If a setting changes during the round — somebody nudges the wind on hole 14 — publish round.settings again, with who changed it in changed_by. While a rules package is in force, keep its request_id on every version.
Why you want it#
Settings do not only change before the round, and a result is meaningless if a change mid-round is not on the record.
Divergence before the round blocks it; divergence during the round flags it and play continues. A mid-round change is recorded and attributable either way, and stopping play would penalize the ordinary case — a bump, a bug, an operator fixing a display.
What it costs you#
Nothing beyond §8a.
12a. Better still: don't let the pinned settings change at all#
What it is#
For the length of a tournament round, refuse to change the settings the rules package pinned. Gray them out, or reject the change.
Why you want it#
Reporting a change tells a committee something went wrong. Refusing one means it didn't. If your software can hold the values, hold them — an event can ask for this directly, and being able to say yes is a reason a facility runs your platform for their league night.
What it costs you#
A flag on the settings your rules-package mapping already touches.
Do it narrowly#
- Only the pinned keys. Not everything. An operator still needs to fix a display mode or a sound device mid-round, and a sim that freezes solid gets uninstalled.
- Say that you locked them, and say which ones you couldn't. List the settings you are actually holding in
round.settings.locked. A lock nobody can see doesn't count, because the engine has no way to guess. - Can't lock something? Say so. If a value is read from a config file at hole load, declaring it unlockable is a perfectly good answer — far better than claiming a lock that doesn't hold. A change to one of those is just a new version of
round.settings, as it would be anyway. - Keep an override, and make it loud. A lock with no escape hatch gets worked around or switched off. An override that publishes a new
round.settingswith the actor inchanged_byis better than no lock at all.
13. Lock out input that is not a swing#
What it is#
On request, disable mouse, keyboard, gamepad and touch as ways to advance a shot — and say so, by declaring only launch_monitor in round.settings.swing_inputs (§8a).
Why you want it#
In a competition with prize money, "he putted with the mouse" is the first thing anybody will ask about. This is the answer.
The rule worth generalizing#
Do not think of this as a list of prohibited assists you have to match. Think of it as:
If you ship a toggle that affects play, you must be able to report its actual state and turn it off on request.
That holds when you add a new assist in the next release: file it under the effect it has (§8a) and it is reportable, and turn-off-able, the day it ships. A list does not.
What it costs you#
An input-mode gate, plus honest reporting of it.
Part 4 — Plumbing#
14. One socket, and forward first#
One persistent connection for the session, on whichever binding you chose. Never connect per message. Set TCP_NODELAY.
And the rule that matters most:
Forward the shot before you log it, render it, or do anything else with it. Nothing in your pipeline may delay a shot reaching the player's screen.
A golfer who hits a ball and waits will blame your software, and they will be right to. Full detail: spec/09.
15. Ship the bundle with your installer#
What it is#
Two files go in every package you ship — updates as well as first installs, not just the first-run installer:
| file | what it is |
|---|---|
| the engine updater | a small executable |
| the standards bundle | a data file, the current version of the standard |
Put both in the same folder as your installer. Your installer checks each one against what is already on the machine and replaces it only if the copy you shipped is newer. Either, both or neither may move.
Why it is asked of you#
Bays go months without internet. If SGA has to reach every machine itself, the standard rots in the field. Instead, every vendor's update channel becomes a distribution channel, and a bay gets brought current by whoever happens to update anything on it.
What it costs you#
Two files in a build artifact; keeping the bundle small is SGA's job (spec/04).
You cannot break anything by shipping an old one. A file older than the copy already on the machine is a safe no-op, because the highest verified version always wins. You will never regress another vendor's work, and they will never regress yours.
And to be clear about what this is not#
The bundle is data and applies itself. The updater is an executable, and it moves only when somebody runs your installer. SGA does not push binaries to bays and this clause does not create a path for it to start (spec/04 §4).
16. How far your rounds count#
Your software is one link in every result's chain — launch monitor, simulator, bay — and the result takes the lowest link's level (the full ladder).
- A0. Ship it. Rounds play, record and sync; they do not count toward a player rating.
- A1. Become a technology partner (free). You promise to report truthfully, declare settings honestly, build nothing that lets a player cheat, and tell SGA about any way you find that would allow it. You also publish a digest of each release. Rounds now count toward an SGA Player Rating and leagues. Run on the same PC as the engine and ship a hashable binary — a stable path, not packed or obfuscated — and the engine can also confirm that the build running is the one you published (
verified_build). It finds your binary from the socket it is talking to and never relies on a path you declare; you may declare one as a cross-check. A simulator on a different PC from the engine cannot be measured, soverified_buildis not available to it. - A2. Also sign your
session.describe— identity, capabilities and the engine's session nonce, as one document — with an Ed25519 key SGA registers, kept so it cannot be pulled out of your binary by simple inspection (02 §2.0a). Sanctioned events. This is the realistic ceiling for simulator software. - A3. The key on a hardware token at each bay. Championships. Possible, and rarely worth it for software.
16a. When to use your own namespace, and when not to#
What it is#
You can define any field you like under x-yourname. and ship it today. No permission, no review, no waiting. Conformance ignores what it does not recognize, so you cannot break anyone by adding one.
There is one rule attached, and it is short:
If a core field already covers it, send the core field.
Send your own version too if yours is richer — x-acme.ball.spin_tilt_deg alongside ball.spin_rpm costs a reader nothing and tells them more. Just don't send only your version of something the catalog already names.
Why we ask#
Your data is worth something to a golfer only if the next bay they walk into can read it. A field called x-acme.bs is invisible to every other participant in the room and to everything downstream — the player rating, the leaderboard, their stats. All the work you did producing that number stops at your own edge.
The core name is what makes it portable. It is the same value either way.
When x- is the right answer#
- A genuinely new sensor class the catalog has never described
- A diagnostic that is nobody's business but yours
- Anything on its way through Tier 2 — ship it now, promote it later
If the core field doesn't fit, tell us#
Wrong unit, wrong sign convention, not enough resolution, semantics that don't match what your hardware actually does — these are real and they happen. That's a Tier 2 proposal, and you're already holding most of what one needs, because you have a shipping implementation.
Say so and the field gets fixed for everyone. Quietly shipping your own version instead means nobody ever finds out the gap exists, and it stays broken.
Check yourself#
cd sga-ois/conformance
python -m sgaconform your-descriptor.json your-scorecard.jsonTesting your integration · Become a technology partner
Where to go next#
- spec/03 — the handshake, for check-in and allowances
- spec/06 — the shot record, for fields 4 and 5
- spec/08 — competition, for everything in Part 3
- spec/09 — transport
- Integrate with SGA in an afternoon — one page, and most of this