SGA‑OISOpen Interoperability Standard
1.0 Draft
SGA-OIS/Specification

Who is playing

Normative.

Who is in the round and who is at the ball; who has checked in before there is a round; what each player receives in a rated match; whether the bay is ready for a stroke; which side the player swings from; and what the simulator is doing for them. These are fields like any other, exchanged under the handshake in 03. They are gathered here because a simulator implements them together, and because each is a question a member's phone or a display asks of the bay.


1. Who is playing, and who just played#

Three fields, because three different things need to know and none of them can work from a slot number alone.

fieldprovided byis
round.player_upthe simulatorwho is at the ball now: slot and label
round.holethe simulatorthe hole in play: number, par, length and course
round.rosterthe simulatorevery player in the round, label to slot
round.previous_strokethe enginewho took the last one
bay.checked_inthe enginewho has scanned in at this bay, before a round exists (§2)
round.allowancesthe enginestrokes received per slot, for rated formats (§5)
bay.round_permittedthe enginewhether a round under an event may begin now, and if not why (08 §2.1)
round.media_entitlementsthe enginewhich slots a camera may keep clips of (11 §5a)
bay.checkin_codethe enginethe check-in code, for a device with a screen to render (§4)
round.settingsthe simulatorwhat is in force: assists classified by what they do to the evidence, conditions, course (§9)

round.player_up is requestable by anybody, and the uses are ordinary: a launch monitor putting the current player on its own display, a HUD projecting the name onto the screen, a statistics board naming whose shot just landed. A device asking for it gets a label and a slot together, because the label is for showing a human and the slot is for attributing data.

round.roster exists for two things nothing else can supply. A display naming the whole group is the obvious one. The one that matters more is check-in: a member scanning the bay's QR code has to pick which of the in-game names is them, and that list has to come from somewhere. The roster persists for the round, so a player who arrives late — or claims their slot after the round has finished — still has something to choose from.

Its mirror, running the other way and before a round exists, is bay.checked_in.

round.hole is one fact about one hole, asserted as one object whenever the hole in play changes. Only number is required:

JSON
{ "type": "record.state", "field": "round.hole",
  "value": { "number": 7, "par": 4, "length_m": 382.0, "course_name": "Example Links",
             "course_holes": 18, "course_par": 72 } }

The engine spreads it across every shot record it composes: number becomes the record's hole, par becomes course.hole_par, length_m becomes course.hole_length_m, and course_name becomes course.name. The course's totals are carried by the course document and the card (06 §9.5), not by each shot.

JSON
{ "round.roster": [
    { "slot": 1, "display_label": "Andrea", "checkin_ref": "9c1f4a2e" },
    { "slot": 2, "display_label": "Sam R" },
    { "slot": 3, "display_label": "guest" } ] }

checkin_ref is where the binding of §2 is recorded. A player who reached the roster by scanning carries the ref of that scan; one whose name was typed carries nothing, and MUST NOT be given one. This is what lets the cloud resolve a position to a member without anybody typing a name or scanning a second time. It is opaque in the bay: nothing here can turn it into a member, and nothing here should try.

round.previous_stroke is the first field the engine itself provides, and it exists for a specific failure. Some data completes long after the stroke it belongs to: swing kinematics that finish processing, a video clip that finishes encoding, a pressure trace that arrives a second late. If the group has rotated in the meantime, that data lands while somebody else is up — and attributing it to whoever is at the ball now silently files one player's swing under another player's name.

So there are two defenses, and a device should use whichever is easier for it:

  • Snapshot at the stroke. A camera or kinematics device that wants round.player_up can record the slot and label at the moment of impact and send them back with the late payload. Simplest, and it needs nothing from us.
  • Ask the engine afterwards. round.previous_stroke names who the last stroke was attributed to, so a device that did not snapshot can still be attributed correctly.

Either way the engine stamps the composed shot record with player_up as it was at the stroke, not as it is when a contributor happens to finish. A late arrival is matched to a stroke, and the stroke already knows whose it was.

2. Check-in, before there is a round#

round.roster runs simulator → engine → phone: the names are typed at round setup, and a member scanning in picks which of them is theirs. That is the right shape for a group that is already playing, and the wrong shape for the moment before. At round setup somebody is standing at a touchscreen typing four names that the association already knows.

bay.checked_in runs the other way. A member scans the bay placard, their app checks them in, and the engine holds them — with the screen name from their SGA account — until they leave or the entry expires. The engine is already running before anyone opens the simulator's menus, so by the time the group reaches player configuration the list is there to be offered.

fieldprovided byis
bay.checked_inthe enginemembers present at this bay who have checked in
JSON
{ "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" } ] }

checkin_ref is opaque and engine-scoped. It is not a member identifier, not derived from one, and not resolvable to one by the bay or by the simulator — 00 §6 is unchanged. It means "the person who scanned in at 17:02", and only the cloud can say who that was.

display_label here is supplied by the member's account rather than typed at a touchscreen. That makes it more accurate and no less a label: it is still not unique, members still collide, and nothing may key on it. Everything 00 §6 says about display_label applies unchanged, including that it is the first field stripped when the claim window closes.

Rules:

  • A simulator MAY use bay.checked_in to pre-fill player configuration. It MUST let the operator edit or remove any pre-filled player. Check-in says a phone was at the bay; it does not say that person is in this round.
  • When a checked-in person is assigned to a slot, the simulator or the engine MUST record the checkin_ref against that slot. That binding is what the cloud resolves to a member, and it is what makes the slot claim happen without anybody typing a name or scanning a second time.
  • A simulator MAY relabel a slot that was seated by check-in — to show the name the player uses in the simulator's own account, for instance. The binding is the checkin_ref on the slot, not the label, so a relabeled slot stays bound and the engine MUST NOT treat a new display_label as a new player.
  • Entries MUST carry expires_at and the engine MUST drop them when it passes. Somebody who scans, plays nothing and leaves must not still be offered to the next group in that bay.
  • The engine MUST drop an entry on explicit check-out.
  • An engine MUST NOT publish bay.checked_in to a device that has not been granted it in the connection matrix. It is a list of who is in the building.
  • Check-in MUST remain optional. A bay whose players never scan works exactly as it does today: names are typed and slots are claimed later.

How the engine learns of a scan. The scan happens on a phone, against the cloud, so the bay is the one participant that is not told — and the bay is where the person is standing. The engine therefore asks. Everything else in this system flows bay to cloud; this is the exception, and it is worth being precise about why it is safe.

A list of who is in a building is not a public fact, and ingest is unauthenticated by design (09). A feed keyed on a bay identifier would be readable by anyone who knows a bay exists, because a bay identifier is printed on a placard. So the engine asks by the rotating check-in code it minted (§4): the code was on a screen in that room for three minutes, and the only people who have seen it were in the room. "Who redeemed the codes I put on my own screen" is a question nothing outside the bay can ask.

  • An engine SHOULD ask about the codes it has shown recently, not only the one in force: a member who scanned four minutes ago used a code that has since rotated.
  • A service answering this MUST scope the answer to the bay, the session and the codes together, and MUST bound how many codes one request may carry — otherwise the request becomes a way to guess them.
  • No credential is created for this. An installation identifier remains an identifier and never becomes a password (02 §3).
  • A person who scanned the code can ask it too, and will see the others who used that same code. They are standing next to those people.

Check-in is already the baseline presence signal in 07 §2.3. This field is what that scan produces locally; the proximity token in the same chapter is what proves the member stayed.

3. A claimed position names itself#

A member who scans before any names are typed is recorded as present with no position, and the bay seats them: the bay assigns positions and the cloud never guesses one (03 §1.1).

The other case is a member picking their own name from the list the bay has already published. That claim names a position, and a position number is only as good as the moment it was chosen. A simulator is free to reorder its roster before a round starts — dragging players into playing order is an ordinary thing for one to let an operator do — and a slot chosen thirty seconds ago may by now be somebody else.

So a claim carries both: the slot the member chose and the claimed_label that was on it when they chose it.

  • A bay MUST NOT bind a claim on the number alone when the label disagrees with what that position now holds. Binding there attributes one player's round to another account, which is the single worst outcome this standard has, and it would do it silently.
  • A bay MAY resolve the disagreement by label: if exactly one position now carries the claimed label, that is where the member meant to be, and binding them there honors the choice they actually made.
  • A bay MAY instead refuse and leave the member present and unplaced. The claim was made against a roster that no longer exists, and asking again costs a scan.
  • A bay that never reorders positions will never see the two disagree, and nothing here asks it to do anything differently.

The pair is the point. Either half alone is ambiguous — a number that may have moved, or a label that two players might share — and together they say the person who was in position 3 when I looked, which is what the member meant.

4. Putting the check-in code on the screen#

Check-in assumes a printed placard on the wall. A simulator has a large screen that people are already looking at, and the player-selection screen is the exact moment somebody wants to check in. So a device that has a display may ask the engine for the check-in code and render it.

fieldprovided byis
bay.checkin_codethe enginethe check-in payload for this bay, to be rendered as a QR code or shown as a short code
JSON
{ "bay.checkin_code": {
    "payload": "https://golfsga.com/c/8F3K2Q",
    "short_code": "8F3K2Q",
    "expires_at": "2026-02-14T18:12:00Z",
    "rotates": true } }

It is delivered in session.manifest, not in session.hello. session.hello is sent before session.describe, which means before the device has presented a bay pairing token and before the engine knows what it is talking to — anything in session.hello goes to whoever opened a socket. The check-in code is the thing that proves somebody was physically at the bay (07 §2.3), and it is the same artifact a device presents to join the bay at all (03 §5). Handing it to an unauthenticated peer would let somebody check in — and be counted present for a ranked round (08 §4) — from a car park, or from anywhere on a facility's flat network. So it goes in the matrix with everything else, and only a device that has already proved where it is can be granted it.

A screen code can rotate, which a printed one cannot. This is the part worth building for. A placard is ink: whatever it encodes is fixed for the life of the sign, so it can only ever say "this is bay 12". A code on a screen can carry a short-lived nonce, which makes a scan evidence that somebody was at this bay in this window rather than at some point since the sign was printed. A simulator that renders this is not just saving a walk to the wall — it is producing a materially stronger presence signal than the placard it replaces.

Rules:

  • bay.checkin_code MUST be granted through the connection matrix and MUST NOT appear in session.hello.
  • A device granted it MAY display it. It MUST NOT log it, persist it beyond display, or forward it to anything else.
  • A device MUST stop displaying a code once expires_at has passed, and SHOULD request a fresh one rather than showing a stale one.
  • When rotates is false the code is equivalent to the printed placard and carries no timing evidence. An engine MUST NOT report rotates: true for a value that does not actually change. A screen code belongs to one round. It is issued for the round in play, or, between rounds, for the round about to open, and it carries which. A lobby code checks somebody in for the round it names before anybody has chosen a course or typed a name; the same code carries on into that round once it opens. A code shown during play (a simulator's players screen, for instance) is for the round in play.
  • When a round closes, the engine MUST stop offering every code it issued for that round, including codes still inside the window it otherwise keeps recently shown codes askable for, and MUST issue the next round's code.
  • An engine that restarts and resumes its session SHOULD pick back up the codes it had shown, so a member who scanned just before the restart is still found and the code on the screen does not change under somebody scanning it. It MAY keep them for that purpose outside the journal, readable only by itself and for no longer than it keeps them askable; it MUST NOT journal them (02 §5.1).
  • The cloud MUST refuse a code for a round that has closed, telling the member the round is over and nothing is joinable with that code. It MUST NOT queue the holder for a later round: a code from a finished game is not evidence of presence at the next one.
  • The cloud MUST treat a code for a round that has not begun as a check-in for that round, so the bay can seat the member when it opens.
  • A printed placard is not a screen code and names no round. Its rules are different: see the placard row of 07 §2.3.
  • Rendering the code is OPTIONAL. A bay whose simulator ignores this field works exactly as it does today, because the placard is still on the wall.
  • A renderer SHOULD encode payload as a QR code at error-correction level H, with the standard four-module quiet zone, in dark-on-light with the full contrast the display can manage, and at the lowest QR version that holds the payload. This is not pedantry about barcodes. A simulator screen is the worst surface a camera phone will ever be asked to read: often projected, frequently at an angle, sometimes with a bright course rendered behind it, and scanned from wherever the player happens to be standing rather than from arm's length. H recovers from about thirty percent damage, which is what buys a scan through glare and a cheap lens. The quiet zone is what a scanner uses to find the code at all, and a code drawn flush against a UI panel frequently will not be found. It is a SHOULD rather than a MUST because a code that scans is the actual requirement, and a renderer with a better answer for its own display should use it. A renderer that cannot meet this SHOULD still display short_code, which needs no camera.

5. Stroke allowances, so a rated match can be set up by itself#

Implementers: the two ways to get this wrong.

  1. Applying allowances when only some players have one. Check complete. False means apply nothing — an unrated player is not a scratch player, and treating them as one hands the match to whoever we happen not to know yet.
  2. Writing net where gross goes. gross is what was actually shot and it never moves. net is reported alongside it. Player ratings post from gross, so a net figure in the gross field makes every future allowance for that member wrong, compounding every round.

See Simulator software integration.

Rated match play needs one number per side before the first tee shot, and today somebody types it in — if they know it at all. When every player in the group has checked in (§2) and the association has already rated them, that number is known before anybody reaches for the keyboard.

round.allowances is engine-provided and arrives after the simulator has committed to a course. It cannot ride along with check-in: an allowance is only meaningful against a course, a tee set and a format, and none of those exist yet when somebody scans the placard at the door. So the simulator declares what is about to be played, and the engine answers.

fieldprovided byis
round.allowancesthe enginestrokes received per slot, for the course and format about to be played
JSON
{ "round.allowances": {
    "basis": { "course_digest": "sha256:1f3c…", "tee_set": "Blue", "format": "match" },
    "complete": true,
    "source": "sga",
    "computed_at": "2026-02-14T18:31:02Z",
    "players": [
      { "slot": 1, "strokes_received": 0 },
      { "slot": 2, "strokes_received": 5, "stroke_holes": [1, 3, 5, 7, 11] } ] } }

It is keyed by slot, not by member. The engine already knows which checkin_ref sits in which slot, because §2 required that binding. Everything downstream of it is a slot number and a small integer, so 00 §6 holds: no index, no account, nothing that outlives the round.

complete is the gate. It is true only when every occupied slot has an allowance. A field where two players are rated and one is not is not a rated match — it is a match with a guess in it, and the standard should not let a simulator quietly split the difference.

stroke_holes is optional and authoritative when present. A simulator knows its own stroke indexes and can allocate strokes_received itself, which is the common case. When the engine supplies the holes — because an event ruleset fixes the allocation, or because the allocation is not the naive one — the simulator MUST use them rather than its own.

An allowance changes the result of a match and nothing else. This is the rule most likely to be implemented wrong, so it is stated as a list of things that MUST NOT move:

stays exactly as playedwhy
every stroke on every holea stroke happened or it did not; an allowance is not evidence about the ball
every shot recordthey are measurements, and no measurement changes because of who somebody is playing
gross and to_parthe card must still show what was actually shot
what the round posts for ratingposting works from gross, or the system feeds on its own output and drifts

What the allowance produces is net, and the match result computed from it. A card that has had allowances applied shows the real strokes and the adjusted outcome, side by side, because a player who shot 84 shot 84.

Sanctioned events are not the casual case. Rated formats are perfectly legitimate in competition — net events are most of amateur golf. The constraint is narrower than "no player ratings in tournaments": in a sanctioned round the allowance MUST come from the Event Rules Package (08 §1) and MUST NOT be a bay-time lookup. An event fixes its allowances in advance, publishes them, and is judged against them. A number fetched at the bay minutes before play is a different claim, and a committee cannot audit what it never saw.

So source: event outranks source: sga whenever both exist. Everything below is for the round that most simulator golf actually is: four people who want a fair match on a Tuesday, and an association that already knows how well each of them plays.

Rules:

  • A simulator MAY pre-apply allowances. It MUST NOT do so when complete is false, and it MUST let the operator override any value.
  • An allowance MUST NOT alter any stroke, any shot record, gross, or what the round posts for rating. It produces net and the match result.
  • Under an Event Rules Package, source MUST be event. An engine MUST NOT substitute an sga allowance into a sanctioned round.
  • The simulator MUST record what it actually applied on the scorecard (strokes_received, stroke_holes, allowance_source). A match result without the strokes that produced it cannot be checked by anybody, which puts it outside everything this standard is for.
  • source MUST say where the number came from — sga, event from the Event Rules Package (08 §1), operator when a human typed it, or local when the simulator computed it from its own stored figures. These are different strengths of evidence and a consumer must be able to tell them apart.
  • An engine that cannot reach the association MUST omit the field or return it with complete: false. A stale allowance is worse than none: it is a wrong result that looks official.
  • Allowances MUST NOT be cached beyond the round they were issued for.

6. Changing the roster while the round is running#

Groups are not fixed. Somebody arrives at the second tee, somebody else has to leave at the seventh, and plenty of simulator software lets an operator add or drop a player without ending the round. Plenty does not. The difference matters enough that it cannot be guessed.

featuremeans
roster_add_in_rounda player may be added after the round has started
roster_remove_in_rounda player may be removed after the round has started
roster_update_in_rounda seated player's tee or display label may be changed, before or during the round
operator_end_roundthe simulator will honor request.end_round (05 §4c)

All four default to false, and absence is a refusal rather than an unknown. A simulator that says nothing gets a roster fixed at the first tee, which is the behavior every implementation already has. An engine MUST NOT send a roster directive to a participant that has not declared the matching feature, and a simulator that receives one anyway MUST refuse it with session.denied.

JSON
{ "type": "request.update_player", "request_id": "q-43", "at": "2026-10-04T19:30:00Z", "asked_by": { "role": "engine" }, "slot": 2, "tee": "White", "display_label": "Sam R." }
{ "type": "request.add_player", "request_id": "q-41", "at": "2026-10-04T19:12:05Z", "asked_by": { "role": "engine" }, "display_label": "Sam R", "checkin_ref": "9c1f4a2e" }
{ "type": "request.remove_player", "request_id": "q-42", "at": "2026-10-04T20:40:11Z", "asked_by": { "role": "operator", "operator_ref": "desk-4f2a91" }, "slot": 3, "reason": "member_left" }

request.update_player changes a seated player, never who they are. It names the slot and carries a new tee, a new display_label, or both, and nothing else: it cannot move a player to another slot or change their checkin_ref. The usual reason is a member on the tee sheet choosing a different tee, or the name shown for them in this game, from their phone. A tee changed after the player has hit their first stroke MUST be refused: the holes already played were played from the tees they were played from.

A player added mid-round starts from the hole in play. The holes already played by the group are not theirs and MUST NOT be backfilled as anything — not zero, not par, not blank strokes. They are simply holes this player has no result for, which 06 §9 already expresses as a hole that is absent from their card. A partial round is a first-class record.

Slots are never renumbered. This is the rule the whole mechanism rests on. Shots have already been journaled against slot 3, and a simulator that closed the gap by moving slot 4 down would reattribute them — silently, after the fact, to a player who never hit them. A removed slot stays removed and its number stays spent for the life of the round.

Joining one is a decision, not a side effect#

A member who scans a bay code mid-round is not obviously asking to join the round in progress. They may be next up, or watching, or have scanned the wrong bay. And joining is not free: the holes the group has already played are holes they will have no result for, so the card they walk away with is a short one.

So a client that offers a mid-round join MUST confirm it, and the confirmation MUST say what the member is joining — which round, and how much of it has already gone:

This round is already on hole 5. Join now? Holes 1-4 will not be on your card.

  • A confirmation MUST NOT be pre-accepted, defaulted to yes, or skipped because the member confirmed something similar earlier.
  • Declining MUST leave them checked in and unplaced. Somebody who scanned and said "not this round" is still standing in the bay, and will usually want the next one.
  • Where the bay has not declared roster_add_in_round, the option MUST NOT be offered at all. Offering something the bay will refuse is worse than offering nothing.

Removal is a withdrawal, not a deletion#

The round happened. A player who leaves at the seventh played six holes and those six stand exactly as golfed: their shots keep their slot, their card keeps its holes, and nothing about the record changes because the person went home. What stops is new strokes.

Removal can start from either end, and both converge on the same observable:

  • The member asks, from their phone — "I am leaving, take me out". The engine sends request.remove_player.
  • The operator does it at the simulator. Nothing is sent, because nothing needs to be: round.roster is standing state (03 §2.2a) and is re-asserted for as long as the round lasts. A slot that is no longer in it has been withdrawn.

That second path is why removal needs no new upward message. A consumer MUST NOT infer withdrawal from silence — a slot that simply stops producing shots is indistinguishable from a player who is slow, a network that dropped, or a sensor that failed. Withdrawal is a positive statement: the roster stops listing them. Absence of shots is not evidence of anything.

Late and early, recorded#

A round where somebody joined at the third and somebody else left at the seventh is a different round from one where four people played all nine, and a scorecard showing two short cards cannot say which happened. So the platform records it against the playing position:

set when
arrived_latethe position first appears on the roster after the round's first stroke
left_earlythe position leaves the roster before the round ends

Both are derived from the roster, not asserted by anybody: they follow from when a slot appeared and when it stopped being listed. Neither changes a score. They exist so that "this member leaves halfway through most of their rounds" is a query rather than an anecdote, and so a player rating review can see a short card for what it is.


7. Ready for a stroke#

Two different things have to be true before a player can hit, and they are held by two different participants.

The simulator is ready when the round is at a point where a shot means something: somebody is up, the camera is not mid-flyover, no menu is open, the ball is teed in the virtual world. The launch monitor is ready when it can see: sensors live and settled, the ball detected in view, no recalibration in progress. Neither can observe the other's answer, and a stroke struck while either is false is the commonest way a real shot goes unrecorded.

"Launch monitor" here means the participant that measures what the stroke did to the ball — the one holding sensor.ball, whatever technology it uses to do so. It observes the ball's launch: speed, angle, direction and spin as it leaves the face. Most also read the club delivering it — face angle, path, angle of attack — and a unit that does holds sensor.club as well.

Camera, radar and infrared units are all launch monitors and all have the same problem: they must be able to see the ball before a stroke can be recorded. That is the question a simulator is asking, and it is not the same question as whether some other sensor in the bay is awake. A bay's environment sensor being unready says nothing about whether a shot can be recorded, and a video capture rig has its own readiness that is a different fact again — if one is ever needed it gets its own field rather than overloading this one.

fieldprovided byis true when
status.sim_readythe simulatorthe round can accept a stroke now
status.lm_readythe launch monitorit can see the ball and record a stroke now

They share the status. namespace because both are a participant's report on its own condition rather than a measurement of anything in the bay — which is also what keeps them out of sim., a namespace this standard does not have. Each is an ordinary standing boolean, asserted and withdrawn like any other (03 §2.2a), carrying a max_age_ms so a consumer can tell a live answer from a stale one.

Two fields and not one. Folding them into a single "armed" flag loses the only thing an operator needs when a shot goes missing, which is which half was not ready.

The engine composes nothing here#

There is no bay.ready, and an engine MUST NOT synthesize one.

The temptation is obvious — take both values, AND them, publish the answer — and it is exactly the thing 00 §7 forbids. "Ready" composed from two sources is no longer either participant's statement; it is the engine's opinion about what they add up to, published under the engine's name, with no way for a reader to recover which half was false or whether either was ever asserted at all. The engine routes these values and records them. It does not decide what they mean together.

Nothing new is needed to make them useful. A simulator that wants to know whether the launch monitor can see puts status.lm_ready in its wants; a launch monitor that wants to know whether the round will accept a shot puts status.sim_ready in its. The matrix connects them like any other field, and each decides for itself what to do with the answer. Where a bay runs more than one launch monitor, status.lm_ready resolves through the matrix as any field does — the readiness that reaches a subscriber is the one from the unit that would actually measure the shot.

A field nobody provides#

No special case, and no interlock to fail open: a want with no provider is already reported. The manifest carries it in unmet with reason no_provider (03 §2.2), at handshake, before a shot is struck.

So a launch monitor that asked for status.sim_ready in a bay whose simulator does not publish it is told so immediately, and behaves exactly as it did before this section existed. A device MUST NOT treat an unmet want as a reason to stop measuring or stop reporting: a bay that answered no questions about readiness has not said it is unready, and a shot that happened still happened (07 §4 — observed absence is not the same as absent observation).

Reconstructing it afterwards#

Both fields are standing values, so both are journaled as they change, with timestamps, like all standing state. Anyone reading the journal can therefore say what each participant claimed at the moment of any stroke, and say it from the record rather than from an engine's summary of it.

That is the whole reason the engine does not compose them: a composed flag would have answered the question once, at write time, in a way nobody could check afterwards. The contributions answer it forever.

8. Handedness, from the two places it is known#

Commercial launch monitors determine handedness, and so can a swing camera. So do simulators. Both answers are real, both are worth having, and the standard carries both rather than picking one: what the game is set for, and what a sensor in the room saw. Where a launch monitor and a camera both report player.lm_handedness, the connection matrix chooses between them as for any other field.

fieldprovided byis
player.sim_handednessthe simulatorwhat the software is configured for — which decides how the shot is rendered and where the ball is teed
player.lm_handednessa sensor in the room — the launch monitor, or a camerawhat it observed — which side of the ball armed (06 §10), or the swing itself (11 §4.2a)

The provenance differs and says so: a menu somebody clicked is inferred, a swing somebody watched is measured. That is the whole of what a consumer needs to rank them, and it is already how every other field in this standard carries that distinction.

These are not two opinions about one field. They are two fields. The same call 06 §4.1 already makes for conditions: a wind value that is a game setting and a wind value that is the weather are not the same fact. A simulator set for a right-hander while a left-hander plays mirrors every number after it, and that is only visible if both answers survive.

Nothing records the disagreement, because nothing merged them. Both values reach the record, and a reader compares them. A flag asserting that two visible values differ would be one more thing that can be wrong about something anyone can check for themselves.

What this makes possible#

The exchange runs in both directions, and that is the point:

  • A monitor sees a left-handed swing and publishes player.lm_handedness. The simulator subscribes, corrects its setup, and publishes the new player.sim_handedness. The bay has followed the player without anybody touching a menu.
  • A monitor that subscribes to player.sim_handedness can tell an operator the simulator is set for the other hand — before a round is scored on it, rather than after.

A single merged field could not have done this, for a structural reason worth stating plainly: an engine never echoes a field to its own producer (03 §3). One field could carry the observation to the simulator, but never the setting back to the monitor. The exchange these exist for would run one way only.

Where a bay publishes just one of them, that is the only answer it has, and the manifest says so in unmet for anything that asked for the other — the ordinary mechanism, with no special case.

9. Settings in force: what the simulator is doing for the player#

Simulators help. Auto-putt, a gimme distance, aim assist, a preview of the putt before it is struck: in casual play that is the product working as intended, and nothing here discourages it. But everything downstream of a round — a player rating, a statistic, a comparison between two members, the result of an event — assumes the player hit the ball, on the course and in the conditions the record describes. A record that cannot say when that was not true cannot be read at all.

fieldprovided byis
round.settingsthe simulatorwhat is in force for the round: its assists, its conditions and its course
JSON
{
  "assists": {
    "complete": true,
    "active": [
      { "id": "gimme", "effect": "skips_the_stroke",
        "applies_to": ["putting"], "distance_m": 0.9 },
      { "id": "x-acme.fairway_finder", "label": "Fairway Finder",
        "effect": "changes_the_result", "applies_to": ["tee_shot"] }
    ]
  },
  "conditions": { "mode": "fixed", "wind_speed_mps": 3.1, "green_speed_stimp": 11.5 },
  "course": { "course_digest": "sha256:...", "tee_set": "Members" },
  "changed_by": "player"
}

One declaration, in every round#

The simulator has one job here: say what is in force. It does that in every round, casual or sanctioned, with this one field. Everything else is a comparison somebody else makes:

  • In a casual round, a rating or a statistic reads the declaration and decides what to do about it.
  • In a sanctioned round, the engine compares the declaration with the event's rules package and signs the result into the journal (08 §2). There is no second report for events: the simulator that already declares its settings for every round has built the whole of what an event asks of it.

A simulator SHOULD publish round.settings before the first stroke of every round, and again whenever anything in it changes. It is a standing value (03 §2.2a), so every change is a new version, and the journal holds them all, each with the moment it took effect. Which strokes were played under which settings is read from the record, not reconstructed, and a change has no message of its own: before and after are the version that was in force and the one that replaced it.

Each version says who made it, in changed_by: the player, an operator, the simulator itself (a default, a restart, an automatic change), or a rules_package. A version that applies an event's package — and every later version while that package is in force — carries its request_id, so a mid-round change is read against what the event required.

Only the simulator may publish it. It is a statement about the simulator's own configuration, and nothing else in the bay is in a position to make it — and because an event compares it with the rules package, a declaration forged by any other participant could make an assisted round look conforming. An engine MUST refuse round.settings from a participant that does not hold the simulator role, whatever that participant declared.

Assists are classified by what they do, not by what they are called#

No list of assists could be complete. Every simulator has its own, they change between releases, and many have no equivalent anywhere else. What does not change is what an assist can do to the evidence, and there are six things:

effectthe recorded stroke…for example
plays_the_strokewas not hit by the playerauto-putt
skips_the_strokewas never playeda gimme distance, a cap on strokes per hole
changes_the_resultwent somewhere the swing did not send itaim assist, shot shaping, automatic wind or elevation compensation
changes_the_coursewas played on an easier hole than the one describeda larger cup, a removed hazard, an improved lie
more_than_one_attemptwas not the player's only trymulligans, shot replay, a preview of the putt or the flight
informs_the_decisionis the player's own, and advice shaped the choicegreen reading, club recommendation

The effects are closed; the assists are not. A consumer writes its policy once, against six effects, instead of against every simulator's settings menu.

applies_to says where on the hole an assist acts — tee_shot (the tee shot on a par 4 or 5), approach (which includes a par-3 tee shot), around_green, putting, or all — because an assist that works on the greens says nothing about the tee shots.

The well-known assists have their effect fixed#

For the assists this standard names, the effect and the scope are part of the definition, and the schema rejects any other:

ideffectapplies toparameter
auto_puttplays_the_strokeputtingdistance_m: the range it takes over within; absent means every putt
gimmeskips_the_strokeputtingdistance_m
max_strokes_per_holeskips_the_strokeallcount
aim_assistchanges_the_resultall
shot_shaping_assistchanges_the_resulttee shot, approach
distance_auto_adjustchanges_the_resultall
wind_auto_compensationchanges_the_resulttee shot, approach, around the green
elevation_auto_compensationchanges_the_resulttee shot, approach, around the green
mulliganmore_than_one_attemptallcount
shot_replaymore_than_one_attemptall
putt_previewmore_than_one_attemptputting
ball_flight_previewmore_than_one_attempttee shot, approach, around the green
green_readinginforms_the_decisionputting
club_recommendationinforms_the_decisiontee shot, approach, around the green

A simulator cannot file auto-putt as advice. Fixing the effect for the well-known names is what makes the classification worth anything: the assists that matter most are exactly the ones whose effect should not be the vendor's to describe.

They are also the vocabulary an event states its requirements in (08 §1), so what an event prohibits or requires and what a round declares can be compared id for id. A gimme distance, a mulligan allowance and a stroke cap are assists with a parameter, not separate rules.

Anything else is the vendor's — and so is its classification#

Every other assist goes in the vendor's namespace, x-<vendor>.<name>, and the vendor declares its effect and scope. label is the vendor's own name for it, for display: untrusted text, rendered escaped, and never decided on.

A vendor could file an assist under a gentler effect than it deserves. Three things stand against that, and none of them is the engine judging it (00 §7):

  • It is attributable. The declaration is journaled against the simulator that made it, under that simulator's identity. A misclassification is a false statement by a named vendor, not an ambiguity in the standard.
  • It is reviewable. An assist a vendor registers through the technology partner portal can be reviewed, and a classification corrected once, for every bay that runs it.
  • It shows in the outcomes. An assist filed as advice that actually moves the ball leaves a trace in what the strokes it touched went on to do. Nothing here needs the vendor's word to be the last one.

Complete, partial, and undeclared#

assists.complete: true is the simulator saying the list is everything in force — and, with an empty active, that nothing is. It is the strongest statement a round can carry about its own evidence, and the only one that can show an event that a prohibited assist was off.

complete: false is the honest answer from a simulator that can report the assists it knows about but cannot swear there are no others. It is worth a great deal more than silence.

A round with no round.settings at all is a third state — undeclared — and a consumer MUST NOT read it as either of the others. In particular, silence is not a declaration that nothing was on. In a sanctioned round, undeclared is what 08 calls unattested.

conditions uses the keys an event package requires them in, and reports the value actually in force, not the value asked for: a simulator set to 11.0 that quantizes to 11.5 declares 11.5. A condition not reported is unsupported, never assumed. course names the course by digest and the tee set in play.

What a consumer does with it is the consumer's decision#

The standard records what was in force and what each assist does. It does not say what a rating, a leaderboard or a statistic should do about it: that is policy, and it belongs to whoever publishes the number (00 §7). An event is the one consumer whose policy the standard does define, because the event's rules package is itself part of the standard.

Simulated Golf Association · SGA-OIS · generated from the specification sources