Skip to main content

AccessDataService

Aggregates every AccessFact a game registers and answers every AccessFeature it gates against them. One place resolves the facts, one place applies the policy, and both realms read the same registry -- so the menu, the storefront and the server's arrival gate reach one verdict instead of three that drift.

Registration belongs in shared code, during Init:

function MyGameAccess:Init(serviceBag)
	local accessDataService = serviceBag:GetService(AccessDataService)

	self._maid:GiveTask(accessDataService:RegisterFact(MyGameFacts.OwnsGame))
	self._maid:GiveTask(accessDataService:RegisterFeature(MyGameFeatures.Chapters))
end

How a fact is decided

Several layers may answer one fact. They are ordered by AccessFactPriority, highest first, and the first layer that contributes decides. What it contributes may be true, false, or unresolved; a layer that abstains (AccessFact.ABSTAIN) is skipped entirely. If nothing contributes, the fact is unresolved.

The merge does not return a value, it returns an AccessFactReport -- every layer, what each said, and which one won -- and the value features read is a field on it. The gate and the debug readout are therefore the same computation, so a readout can never explain a decision that was not the one made.

What is deliberately not here

Facts are addressable by name so they can be inspected and overridden, but there is no ObserveFact. You can set a fact and you can look at one; you cannot subscribe to one and write your own rule at a call site. That asymmetry is what keeps every consumer reading the same verdict, which is the entire reason this is a package rather than four call sites that each decide for themselves.

Types

AccessFactLayerReport

interface AccessFactLayerReport {
sourcestring
prioritynumber
contributesboolean
valueboolean?--

meaningful only when contributes

decidedboolean--

true for the single layer that won

}

What one layer said, and whether it was the one that decided.

contributes is the distinction the whole merge turns on: a layer that abstained said nothing and was skipped, whereas a layer that contributed nil said "nobody knows yet" and stopped the fall-through.

AccessFactReport

interface AccessFactReport {
factNamestring
valueboolean?--

what features read

decidedBystring?--

nil when every layer abstained

}

One fact, fully explained: what features read, who decided it, and what every layer said. layers is ordered highest priority first, so it reads top-down the way the merge ran.

AccessFeatureReport

interface AccessFeatureReport {
featureNamestring
stateAccessState
facts{[string]AccessFactReport}
}

A verdict together with every fact it was reached from -- the whole answer to "why can this player not get in", in one place.

Functions

SetReplicatedFeatureFactNames

AccessDataService.SetReplicatedFeatureFactNames(
payload{[string]{string}}
) → ()

Applies what the server says each feature reads. The entry point the client's replication arrives through, and the seam a test drives directly.

Facts named here need no resolver in this realm -- an unregistered fact reads as unresolved locally and takes its answer from the per-player replication, which is the whole point of being told about it.

HasFeature

AccessDataService.HasFeature(
featureNamestring
) → boolean

IsFeatureAllowedByName

AccessDataService.IsFeatureAllowedByName(
playerPlayer,
featureNamestring,
subjectany?
) → boolean

Whether the player may use this feature, addressed by name, as a plain boolean. Unresolved reads as false: a caller working from a string wants an answer, and failing closed is the safe half.

Prefer the AccessFeature object where you have one -- a typo in a require is an error, a typo in a string is a feature that quietly denies.

ObserveIsFeatureAllowedByName

AccessDataService.ObserveIsFeatureAllowedByName(
playerPlayer,
featureNamestring,
subjectany?
) → Observable<boolean>

PromiseIsFeatureAllowedByName

AccessDataService.PromiseIsFeatureAllowedByName(
playerPlayer,
featureNamestring,
subjectany?
) → Promise<boolean>

ObserveFeatureAllowedStateByName

AccessDataService.ObserveFeatureAllowedStateByName(
playerPlayer,
featureNamestring,
subjectany?
) → Observable<AccessState>

The full verdict for a feature addressed by name, so a caller can tell a refusal from a non-answer.

TeardownPlayer

AccessDataService.TeardownPlayer(
playerPlayer
) → ()

Releases everything held for a player: their overrides, every fact layer's cached resolution, and any promise still waiting on a verdict for them.

Driven by AccessService off Players.PlayerRemoving in a real game, and callable directly by a test or by anything holding a mock rather than a real Player -- so a session ends deterministically instead of waiting on the collector.

Deliberately not wired to a player-observing utility in here: this service is shared, and every such utility depends on playermock, which this package already depends on for its own tests. Two paths to one module duplicates it in a built place and the loader can no longer resolve the name.

ObserveIsPlayerPresent

AccessDataService.ObserveIsPlayerPresent(
playerPlayer
) → Observable<boolean>

Whether the player is still here. False forever once they have left.

RegisterFact

AccessDataService.RegisterFact() → () → ()--

Removes the layer

Registers a layer of a fact.

Two layers of the same fact must differ in both priority and source. Equal priorities would make the winner depend on registration order, and equal sources would leave a readout unable to say which layer decided -- both are refused at registration rather than discovered while someone is complaining.

Registering does not take ownership. A fact has a lifetime of its own, so give it to a maid at the point you make it and the disposer to a maid too.

AddAccessFact

AccessDataService.AddAccessFact(
factNamestring,
valueValueObject.Mountable<boolean?>,
) → () → ()--

Removes the layer and destroys the fact

Registers a fact from a value instead of a resolver -- one answer for every player.

Prefer AccessDataService.RegisterFact with a resolver declared in shared code. Whoever calls this owns which realm has the fact, and a server-only call leaves the client with a fact that never resolves, which stalls every feature declaring it.

Unlike AccessDataService.RegisterFact, the returned disposer also destroys the fact: it is made here and never handed out, so there is no call site that could own it.

RegisterFeature

AccessDataService.RegisterFeature() → () → ()--

Removes the registration

GetFactLayers

AccessDataService.GetFactLayers(
factNamestring
) → {AccessFact}

The layers registered for a fact, highest priority first.

HasFact

AccessDataService.HasFact(
factNamestring
) → boolean

GetFeature

AccessDataService.GetFeature(
featureNamestring
) → AccessFeature?

The feature registered under this name, for console commands and anything else working from a string. Code should hold the AccessFeature itself -- a typo in a require is an error, a typo in a string is a feature that quietly denies.

GetFactNames

AccessDataService.GetFactNames(selfAccessDataService) → {string}

GetFeatureNames

AccessDataService.GetFeatureNames(selfAccessDataService) → {string}

ObserveFeatureNames

AccessDataService.ObserveFeatureNames(selfAccessDataService) → Observable<{string}>

Every registered feature name, live. Changes as a game registers more.

ObserveFeature

AccessDataService.ObserveFeature(
playerPlayer,
featureAccessFeature,
subjectany?--

Passed to the feature's compute, for per-thing features

) → Observable<AccessState>

Whether the player may use this feature, live.

Opens on unresolved rather than on nothing, so a consumer always has a state to render. Repeats of the same verdict are suppressed.

ObserveIsAllowed

AccessDataService.ObserveIsAllowed(
playerPlayer,
featureAccessFeature,
subjectany?
) → Observable<boolean>

Whether the player may use this feature, as a plain boolean.

PromiseFeature

AccessDataService.PromiseFeature(
playerPlayer,
featureAccessFeature,
subjectany?
) → Promise<AccessState>

The verdict once it actually settles, skipping unresolved.

GOTCHA: stays pending for as long as the answer stays unknown. That is deliberate -- a gate that resolved on a non-answer would be deciding by coin toss -- but it means a caller that must act within a bounded time has to impose its own timeout and decide what a timeout means for it.

SetFactOverride

AccessDataService.SetFactOverride(
playerPlayer,
factNamestring,
valueboolean?
) → () → ()--

Clears this override

Forces a fact for this player, whatever its layers say. Pass nil to force unresolved, which is the state hardest to reproduce by hand and the one that strands players.

Sits above every other layer, so an override never has to be reconciled against whatever a game added later. It shows up in a report as its own layer, with the layers it outranked still listed underneath -- so nobody mistakes an override left on for a genuine entitlement.

Server-authoritative: a client that could set this could grant itself anything. Overrides are for tests and for console commands run by someone who already has permission.

ClearFactOverride

AccessDataService.ClearFactOverride(
playerPlayer,
factNamestring
) → ()

ClearFactOverrides

AccessDataService.ClearFactOverrides(
playerPlayer
) → ()

ObserveFactReport

AccessDataService.ObserveFactReport(
playerPlayer,
factNamestring
) → Observable<AccessFactReport>

One fact, fully explained. The readout a console command renders, and the answer to "denied, but on account of what".

ObserveFactReports

AccessDataService.ObserveFactReports(
playerPlayer
) → Observable<{[string]AccessFactReport}>

Every registered fact for this player, fully explained.

ObserveFactNames

AccessDataService.ObserveFactNames(selfAccessDataService) → Observable<{string}>

The registered fact names, live. Emits again whenever a layer is registered or removed.

ObserveFeatureReport

AccessDataService.ObserveFeatureReport(
playerPlayer,
featureAccessFeature,
subjectany?
) → Observable<AccessFeatureReport>

A feature's verdict together with every fact it was reached from -- the whole answer to a complaint, in one subscription.

GetDebugState

AccessDataService.GetDebugState(selfAccessDataService) → {
facts{[string]{{
factNamestring,
prioritynumber,
sourcestring
}}},
features{[string]{
featureNamestring,
facts{string}
}}
}

Everything registered, as plain tables. Player-independent -- for "what does this game gate, and what answers it", which is the question you have before you have a player to ask about.

Live per-player state is AccessDataService.ObserveFactReports, and the console renders both.

SetServerFactValue

AccessDataService.SetServerFactValue(
playerPlayer,
factNamestring,
valueboolean?,
abstainedboolean?,
metadataany?
) → ()

Records what the server says a fact reads as for this player. Called by the client's replication receiver; the server itself never calls it.

Replication is unconditional -- every fact's server answer is sent -- and what a client does with it is the fact's AccessFactServerOverrideBehavior. Splitting it that way means a fact can never accidentally not replicate, only decline to be overridden by what arrived.

ObserveFactOverrides

AccessDataService.ObserveFactOverrides(
playerPlayer
) → Observable<{[string]{valueboolean?}}>

The overrides in force for this player in this realm, live.

What AccessPlayer replicates. A box is present for every overridden fact, and its value is absent when the override forces unresolved -- which is a thing somebody deliberately does, and so has to survive the trip rather than looking like no override at all.

SetReplicatedFactOverrides

AccessDataService.SetReplicatedFactOverrides(
playerPlayer,
entries{[string]{valueboolean?}}
) → ()

Applies the overrides the server has in force. The entry point the client's replication arrives through, and the seam a test drives directly.

Overrides are debugging instructions, not entitlements, and an instruction that only took effect in one realm would be the worst of both: a console session that opens the server's gate while the client still renders it shut, with nothing in either readout saying why.

An override set locally shadows one that arrives here, so a person investigating in this realm is not fighting somebody else's console.

Show raw api
{
    "functions": [
        {
            "name": "SetReplicatedFeatureFactNames",
            "desc": "Applies what the server says each feature reads. The entry point the client's replication arrives\nthrough, and the seam a test drives directly.\n\nFacts named here need no resolver in this realm -- an unregistered fact reads as unresolved locally and\ntakes its answer from the per-player replication, which is the whole point of being told about it.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "payload",
                    "desc": "",
                    "lua_type": "{ [string]: { string } }"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 325,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "HasFeature",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "featureName",
                    "desc": "",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 432,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "IsFeatureAllowedByName",
            "desc": "Whether the player may use this feature, addressed by name, as a plain boolean. Unresolved reads as\nfalse: a caller working from a string wants an answer, and failing closed is the safe half.\n\nPrefer the [AccessFeature] object where you have one -- a typo in a require is an error, a typo in a\nstring is a feature that quietly denies.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "featureName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 450,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveIsFeatureAllowedByName",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "featureName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<boolean>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 473,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "PromiseIsFeatureAllowedByName",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "featureName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise<boolean>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 491,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFeatureAllowedStateByName",
            "desc": "The full verdict for a feature addressed by name, so a caller can tell a refusal from a non-answer.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "featureName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<AccessState>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 513,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "TeardownPlayer",
            "desc": "Releases everything held for a player: their overrides, every fact layer's cached resolution, and any\npromise still waiting on a verdict for them.\n\nDriven by [AccessService] off `Players.PlayerRemoving` in a real game, and callable directly by a test\nor by anything holding a mock rather than a real Player -- so a session ends deterministically instead\nof waiting on the collector.\n\nDeliberately not wired to a player-observing utility in here: this service is shared, and every such\nutility depends on `playermock`, which this package already depends on for its own tests. Two paths to\none module duplicates it in a built place and the loader can no longer resolve the name.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 541,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveIsPlayerPresent",
            "desc": "Whether the player is still here. False forever once they have left.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<boolean>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 570,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "RegisterFact",
            "desc": "Registers a layer of a fact.\n\nTwo layers of the same fact must differ in both priority and source. Equal priorities would make the\nwinner depend on registration order, and equal sources would leave a readout unable to say which layer\ndecided -- both are refused at registration rather than discovered while someone is complaining.\n\nRegistering does not take ownership. A fact has a lifetime of its own, so give it to a maid at the\npoint you make it and the disposer to a maid too.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "fact",
                    "desc": "",
                    "lua_type": "AccessFact"
                }
            ],
            "returns": [
                {
                    "desc": "Removes the layer",
                    "lua_type": "() -> ()"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 617,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "AddAccessFact",
            "desc": "Registers a fact from a value instead of a resolver -- one answer for every player.\n\nPrefer [AccessDataService.RegisterFact] with a resolver declared in shared code. Whoever calls this\nowns which realm has the fact, and a server-only call leaves the client with a fact that never\nresolves, which stalls every feature declaring it.\n\nUnlike [AccessDataService.RegisterFact], the returned disposer also destroys the fact: it is made here\nand never handed out, so there is no call site that could own it.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "value",
                    "desc": "",
                    "lua_type": "ValueObject.Mountable<boolean?>"
                },
                {
                    "name": "options",
                    "desc": "",
                    "lua_type": "AccessFactOptions?"
                }
            ],
            "returns": [
                {
                    "desc": "Removes the layer and destroys the fact",
                    "lua_type": "() -> ()"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 742,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "RegisterFeature",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "feature",
                    "desc": "",
                    "lua_type": "AccessFeature"
                }
            ],
            "returns": [
                {
                    "desc": "Removes the registration",
                    "lua_type": "() -> ()"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 764,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "GetFactLayers",
            "desc": "The layers registered for a fact, highest priority first.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ AccessFact }"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 803,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "HasFact",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 815,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "GetFeature",
            "desc": "The feature registered under this name, for console commands and anything else working from a string.\nCode should hold the [AccessFeature] itself -- a typo in a require is an error, a typo in a string is a\nfeature that quietly denies.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "featureName",
                    "desc": "",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "AccessFeature?"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 829,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "GetFactNames",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ string }"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 838,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "GetFeatureNames",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ string }"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 851,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFeatureNames",
            "desc": "Every registered feature name, live. Changes as a game registers more.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<{ string }>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 863,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFeature",
            "desc": "Whether the player may use this feature, live.\n\nOpens on unresolved rather than on nothing, so a consumer always has a state to render. Repeats of the\nsame verdict are suppressed.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "feature",
                    "desc": "",
                    "lua_type": "AccessFeature"
                },
                {
                    "name": "subject",
                    "desc": "Passed to the feature's compute, for per-thing features",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<AccessState>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 878,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveIsAllowed",
            "desc": "Whether the player may use this feature, as a plain boolean.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "feature",
                    "desc": "",
                    "lua_type": "AccessFeature"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<boolean>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 948,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "PromiseFeature",
            "desc": "The verdict once it actually settles, skipping unresolved.\n\nGOTCHA: stays pending for as long as the answer stays unknown. That is deliberate -- a gate that\nresolved on a non-answer would be deciding by coin toss -- but it means a caller that must act within a\nbounded time has to impose its own timeout and decide what a timeout means for it.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "feature",
                    "desc": "",
                    "lua_type": "AccessFeature"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise<AccessState>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 972,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "SetFactOverride",
            "desc": "Forces a fact for this player, whatever its layers say. Pass nil to force unresolved, which is the\nstate hardest to reproduce by hand and the one that strands players.\n\nSits above every other layer, so an override never has to be reconciled against whatever a game added\nlater. It shows up in a report as its own layer, with the layers it outranked still listed underneath\n-- so nobody mistakes an override left on for a genuine entitlement.\n\nServer-authoritative: a client that could set this could grant itself anything. Overrides are for\ntests and for console commands run by someone who already has permission.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "value",
                    "desc": "",
                    "lua_type": "boolean?"
                }
            ],
            "returns": [
                {
                    "desc": "Clears this override",
                    "lua_type": "() -> ()"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1015,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ClearFactOverride",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 1056,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ClearFactOverrides",
            "desc": "",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 1078,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFactReport",
            "desc": "One fact, fully explained. The readout a console command renders, and the answer to \"denied, but on\naccount of what\".",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<AccessFactReport>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1095,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFactReports",
            "desc": "Every registered fact for this player, fully explained.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<{ [string]: AccessFactReport }>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1207,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFactNames",
            "desc": "The registered fact names, live. Emits again whenever a layer is registered or removed.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<{ string }>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1233,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFeatureReport",
            "desc": "A feature's verdict together with every fact it was reached from -- the whole answer to a complaint,\nin one subscription.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "feature",
                    "desc": "",
                    "lua_type": "AccessFeature"
                },
                {
                    "name": "subject",
                    "desc": "",
                    "lua_type": "any?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<AccessFeatureReport>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1246,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "GetDebugState",
            "desc": "Everything registered, as plain tables. Player-independent -- for \"what does this game gate, and what\nanswers it\", which is the question you have before you have a player to ask about.\n\nLive per-player state is [AccessDataService.ObserveFactReports], and the console renders both.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService\n"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ facts: { [string]: { { factName: string, priority: number, source: string } } }, features: { [string]: { featureName: string, facts: { string } } } }"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1288,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "_mergeContributions",
            "desc": "The merge, as a pure function: layers highest priority first, the first that contributes decides.",
            "params": [
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "contributions",
                    "desc": "",
                    "lua_type": "{ { source: string, priority: number, contribution: AccessFactContribution } }"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "AccessFactReport"
                }
            ],
            "function_type": "static",
            "private": true,
            "source": {
                "line": 1354,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "SetServerFactValue",
            "desc": "Records what the server says a fact reads as for this player. Called by the client's replication\nreceiver; the server itself never calls it.\n\nReplication is unconditional -- every fact's server answer is sent -- and what a client *does* with\nit is the fact's [AccessFactServerOverrideBehavior]. Splitting it that way means a fact can never\naccidentally not replicate, only decline to be overridden by what arrived.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "factName",
                    "desc": "",
                    "lua_type": "string"
                },
                {
                    "name": "value",
                    "desc": "",
                    "lua_type": "boolean?"
                },
                {
                    "name": "abstained",
                    "desc": "",
                    "lua_type": "boolean?"
                },
                {
                    "name": "metadata",
                    "desc": "",
                    "lua_type": "any?\n"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 1507,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "ObserveFactOverrides",
            "desc": "The overrides in force for this player in *this* realm, live.\n\nWhat [AccessPlayer] replicates. A box is present for every overridden fact, and its `value` is absent\nwhen the override forces unresolved -- which is a thing somebody deliberately does, and so has to\nsurvive the trip rather than looking like no override at all.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Observable<{ [string]: { value: boolean? } }>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 1580,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "SetReplicatedFactOverrides",
            "desc": "Applies the overrides the server has in force. The entry point the client's replication arrives\nthrough, and the seam a test drives directly.\n\nOverrides are debugging instructions, not entitlements, and an instruction that only took effect in one\nrealm would be the worst of both: a console session that opens the server's gate while the client still\nrenders it shut, with nothing in either readout saying why.\n\nAn override set locally shadows one that arrives here, so a person investigating in this realm is not\nfighting somebody else's console.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "AccessDataService"
                },
                {
                    "name": "player",
                    "desc": "",
                    "lua_type": "Player"
                },
                {
                    "name": "entries",
                    "desc": "",
                    "lua_type": "{ [string]: { value: boolean? } }"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 1603,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        }
    ],
    "properties": [],
    "types": [
        {
            "name": "AccessFactLayerReport",
            "desc": "What one layer said, and whether it was the one that decided.\n\n`contributes` is the distinction the whole merge turns on: a layer that abstained said nothing and was\nskipped, whereas a layer that contributed `nil` said \"nobody knows yet\" and stopped the fall-through.",
            "fields": [
                {
                    "name": "source",
                    "lua_type": "string",
                    "desc": ""
                },
                {
                    "name": "priority",
                    "lua_type": "number",
                    "desc": ""
                },
                {
                    "name": "contributes",
                    "lua_type": "boolean",
                    "desc": ""
                },
                {
                    "name": "value",
                    "lua_type": "boolean?",
                    "desc": "meaningful only when contributes"
                },
                {
                    "name": "decided",
                    "lua_type": "boolean",
                    "desc": "true for the single layer that won"
                }
            ],
            "source": {
                "line": 83,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "AccessFactReport",
            "desc": "One fact, fully explained: what features read, who decided it, and what every layer said. `layers` is\nordered highest priority first, so it reads top-down the way the merge ran.",
            "fields": [
                {
                    "name": "factName",
                    "lua_type": "string",
                    "desc": ""
                },
                {
                    "name": "value",
                    "lua_type": "boolean?",
                    "desc": "what features read"
                },
                {
                    "name": "decidedBy",
                    "lua_type": "string?",
                    "desc": "nil when every layer abstained"
                },
                {
                    "name": "layers",
                    "lua_type": "{ AccessFactLayerReport }",
                    "desc": ""
                }
            ],
            "source": {
                "line": 104,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        },
        {
            "name": "AccessFeatureReport",
            "desc": "A verdict together with every fact it was reached from -- the whole answer to \"why can this player not\nget in\", in one place.",
            "fields": [
                {
                    "name": "featureName",
                    "lua_type": "string",
                    "desc": ""
                },
                {
                    "name": "state",
                    "lua_type": "AccessState",
                    "desc": ""
                },
                {
                    "name": "facts",
                    "lua_type": "{ [string]: AccessFactReport }",
                    "desc": ""
                }
            ],
            "source": {
                "line": 130,
                "path": "src/access/src/Shared/AccessDataService.lua"
            }
        }
    ],
    "name": "AccessDataService",
    "desc": "Aggregates every [AccessFact] a game registers and answers every [AccessFeature] it gates against them.\nOne place resolves the facts, one place applies the policy, and both realms read the same registry --\nso the menu, the storefront and the server's arrival gate reach one verdict instead of three that drift.\n\nRegistration belongs in shared code, during `Init`:\n\n```lua\nfunction MyGameAccess:Init(serviceBag)\n\tlocal accessDataService = serviceBag:GetService(AccessDataService)\n\n\tself._maid:GiveTask(accessDataService:RegisterFact(MyGameFacts.OwnsGame))\n\tself._maid:GiveTask(accessDataService:RegisterFeature(MyGameFeatures.Chapters))\nend\n```\n\n## How a fact is decided\n\nSeveral layers may answer one fact. They are ordered by [AccessFactPriority], highest first, and **the\nfirst layer that contributes decides**. What it contributes may be `true`, `false`, or unresolved; a\nlayer that abstains ([AccessFact.ABSTAIN]) is skipped entirely. If nothing contributes, the fact is\nunresolved.\n\nThe merge does not return a value, it returns an [AccessFactReport] -- every layer, what each said, and\nwhich one won -- and the value features read is a field on it. The gate and the debug readout are\ntherefore the same computation, so a readout can never explain a decision that was not the one made.\n\n## What is deliberately not here\n\nFacts are addressable by name so they can be inspected and overridden, but there is no `ObserveFact`.\nYou can set a fact and you can look at one; you cannot subscribe to one and write your own rule at a\ncall site. That asymmetry is what keeps every consumer reading the same verdict, which is the entire\nreason this is a package rather than four call sites that each decide for themselves.",
    "source": {
        "line": 38,
        "path": "src/access/src/Shared/AccessDataService.lua"
    }
}