Groups
A session holds a whole set of entities that share a key โ a town and its buildings โ while its observer meets the town, and is told when it holds all of it.
Status: ๐ง Partial โ region, explicit and shared sets on Sphere observers, positioned and positionless members ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Subscriptions
๐ฏ What it solves
A Sphere around the player answers "what is near me". Some sets are anchored on something else: "the buildings of this town" is defined by the town, not by the observer. At a town's north edge a sphere misses its south edge and fills the rest with desert, and no radius expresses the set. A group set names the set by a key the members carry โ the town each building belongs to โ and a session holds every member of a town while it is in or near it, with a completion signal a client can act on: once a town is complete, every building of it is in the store.
โ๏ธ How it works (in brief)
- Membership is the engine's own index. The key field carries
[Index(AllowMultiple = true, Sparse = true)]: 0 is "no group" and is not indexed, every commit and fence path maintains it, a crash rebuilds it. Replication reads that index to fill a town and keeps no copy. - A region set names a region archetype โ one entity per town, carrying the key and an extent (
AABB2F,AABB3F,BSphere2F,BSphere3F). A session's Sphere meets a town when its anchor is within the declared reach of the extent (0 inside it), keeps it within reach plus the set's leave margin, and releases it beyond. A town met again is filled and completed again. - Fill under the enter budget. A town's members enter in ascending netId order, sharing the frame's enter budget with the cells; the
frame carrying the last one also carries
GROUP_COMPLETE{set, key}. A release carriesGROUP_RELEASED{set, key}with the leaves of the members nothing else holds. ARESETreleases everything; the towns still met are filled and completed again. - Union with geometry. A member the Sphere also holds is one entity in the store; a member moving between two held towns is an update.
- Key changes need no push. Writing a member's key component marks it like a position write, so the projection sees the new key the same tick.
- A client can ask. An explicit set's keys are the ones a session's client requested (
SubscribeRequest, built-in command 1) and the application allowed, plus the ones a system chose (SetSources). Each request is answered inSOURCES:SOURCE_APPLIEDonce its key is complete, orSOURCE_ERRORwith a code โ the engine's below 100, the application's from 100. A shared set has one key every session naming it holds. - Members need no position. An archetype with none (a structure's shards, a joint record) is a member like any other, reached through its keys alone. It is in no realm and in no cell, and it has replication state only while some session holds a key it carries: a cluster of it gets a block when a held key needs one, and gives it back once unneeded for 64 ticks. A key completes only once every member has that state and has entered. A Versioned key on such a member may be written off the tick; the fill reads the commits' moves beside the index, so none is lost.
- A positioned member's key is its realm's; a positionless member's is engine-wide. A positioned member โ a building with a place in a realm โ
is held only by sessions of its own realm: one map spawned in several realms with the same keys stays isolated, each realm's town 7 holding its own
buildings. A positionless member belongs to no realm, so its key is one group wherever it is held: two realms whose regions use key 7 โ or whose
sessions subscribe to it โ see the same positionless members. That is how data is shared across realms (a guild roster seen from every instance);
an application that wants positionless data isolated gives each instance its own keys, and an explicit set's
admitdecides who may ask.
๐ป Usage
[Component("Game.Town", 1, StorageMode = StorageMode.SingleVersion)]
struct TownOf { [Index(AllowMultiple = true, Sparse = true)] public int Town; } // 0 = in no town
[Component("Game.Zone", 1, StorageMode = StorageMode.SingleVersion)]
struct Zone { public int Key; public AABB2F Extent; }
subs.Groups("town", g => g
.Region<TownRegion>(r => r.Key(TownRegion.Zone, z => z.Key).Extent(TownRegion.Zone, z => z.Extent))
.Member<Building>(m => m.Key(Building.Town, t => t.Town))
.Leave(30)); // hysteresis, metres
subs.Profile("player", p => p.Sphere(192).Of<Creature>().Groups("town", reachM: 50));
var options = new SubscriptionsOptions { MaxGroupsPerSession = 8 /* required with any set */ };
// A member with no position: no Position or Motion, a member of the set, named by no observer.
subs.Archetype<Shard>(a => a.Field(Shard.Data, d => d.Material, Codec.U8, name: "material"));
subs.Groups("structure", g => g
.Region<StructureGroup>(r => r.Key(StructureGroup.Bounds, b => b.Id).Extent(StructureGroup.Bounds, b => b.Box))
.Member<Structure>(m => m.Key(Structure.Group, s => s.Id))
.Member<Shard>(m => m.Key(Shard.Group, s => s.Id)));
// An explicit set: keys a client asks for, allowed or not by the application; a shared one: one key for every session.
subs.Groups("myLists", g => g
.Explicit((in GroupRequest r) => Owns(r.Session, r.Key) ? GroupDecision.Allow : GroupDecision.Deny(403))
.Member<Listing>(m => m.Key(Listing.Owner, o => o.Account)));
subs.Groups("bazaar", g => g.Shared(1).Member<Listing>(m => m.Key(Listing.Listed, l => l.Board)));
subs.Profile("trader", p => p.Sphere(64).Groups("myLists").Groups("bazaar"));
ctx.Subscriptions.Session(session).SetSources("myLists", accountId); // the server's keys, beside the client's
On the client: store.IsGroupComplete(set, key), store.CompleteGroups and the frame's store.Groups (.NET);
applier.groups.isComplete(set, key) (TypeScript); typhon_group_is_complete and typhon_groups_get (C). The set's index is its position
in the catalog's groupSets, in name order. Whether a Region set's selection is whole โ every group the observer meets held โ is
store.GroupSetOverLimit(set) == 0 (.NET), applier.groups.overLimit(set) === 0 (TypeScript), typhon_group_set_over_limit (C).
Asking for a key, in each SDK โ the answer arrives once the key's members are in the store:
var answer = await client.SubscribeAsync("myLists", accountId); // .NET: Applied, Refused (answer.Code), Rejected or Dropped
await client.UnsubscribeAsync("myLists", accountId);
const subs = new GroupSubscriptions(session.plan); // TypeScript: pump before each flush, observe after each frame
const answer = await subs.subscribe('myLists', accountId); // { outcome: 'applied' | 'refused' | 'rejected' | 'dropped', code, โฆ }
uint16_t id; /* C: sent by the next typhon_commands_flush */
typhon_group_subscribe(client, set, account_id, &id);
/* after a poll: typhon_subscribe_results_get(client, &results, &count) โ outcome TYPHON_SUBSCRIBE_APPLIED, _REFUSED, _REJECTED or _DROPPED */
Requests leave in call order, paced by the command's rate; one the server still refuses for its rate goes again by itself; a session that ends drops what is still waiting โ a new session holds none of it.
โ ๏ธ Guarantees & limits
- Refused at
Start: a set with no region, groups on aWorldorClientRegionobserver, a key that is not an integer, not sparse and multi-value, on aTransientcomponent (#1162), or of another signedness than its region key, a member with no projection, an observer naming an archetype with no position, more than four sets per archetype or 256 per runtime, an extent of another type, and a declaration withoutMaxGroupsPerSession(at most 1 048 576 โ a rail against a mistyped value, not a cost). - A region in a realm other than 0 needs a positioned region archetype: one with no spatial field has no realm, and its regions are realm 0's.
- A profile may name groups and no archetype: its Sphere then holds the towns' members and nothing by geometry.
- A member held through a group stays held when the Sphere narrows or moves away from it: it leaves only once neither a held key nor the geometry holds it. A test that asserts what a Sphere holds sees the town's members too, the day a set is declared โ stand the session outside every town to test the sphere alone.
MaxGroupsPerSessionis a bound, not a reservation: a session's group state costs about 12 B a slot, committed and pending each, its slots at most four times the keys it holds; its per-frame group work grows with those keys โ never with the bound (#1407). It bounds the keys admitted: a key on its way out (below) can keep a session above it for a few frames.- Past
MaxGroupsPerSessiontowns met at once, the kept ones and then the nearest are held and the rest counted; the session is told how many it left out (GROUP_TRUNCATED{set, over}, again after aRESET, 0 once it holds every town it meets), so a client can tell a whole selection from a cut one โGROUP_COMPLETEonly ever says one group is whole. - A frame completes at most
EnterBudgetPerFramegroups and releases at most as many โ a completion costs a unit of the budget even with no member, and a group past the release budget stays held until a later frame releases it โ so thousands of groups met or given back at once spread over frames. - Explicit and shared sets: a profile names them with
Groups(set), no reach; at mostMaxGroupsPerSessionkeys per set, a client's and the server's together;SubscribeRequestis rate-limited to 5 a second past a burst ofMaxGroupsPerSessionclamped to 5โ1 024 (the catalog says so: a login may subscribe every key it may hold at once, up to 1 024), and a request past it โ or past the answers a session may be owed, twice the burst and at least 64 โ is anACK, not an answer. A handler that throws or returnsdefaultrefuses the requestDenied; a subscription still filling when the session's profile stops naming the set is answeredNotInProfile. A realm-kind variant names the same explicit sets as its profile. - Visibility tags apply to members as to everything else (Profiles & observers): a town is complete when every member the session sees has entered, and a hidden member is never sent.
- Measured (
GroupRegionBench, 14 ยง 3.6): a 456 m town on a 192 m Sphere costs no measurable gather time (within 1 ยตs over 128 sessions) and no steady-state bytes; widening the Sphere to 456 m instead sends 2.8ร the bytes.
๐งช Tests
- PushGroupTests โ the oracle (towns, members and walking sessions under churn and skips), hysteresis and re-entry, budgeted fill, moves between towns, a destroyed town, the refusals, the tap's mutant
- PushGroupShardTests โ positionless members: state only while held, the budget, a reference resolved once its target has state, #1389's split and merge, an off-DAG key move between the projection and the gather
- LiveGroupSubscriptionTests โ
SubscribeAsyncover TCP to a live engine: applied after the completion, the application's code, a login burst, a release, dropped on close - The
subscribe-requestsvector โ one script of requests, pumps, frames and a close, replayed by the .NET, TypeScript and native helpers - PushGroupRequestTests โ explicit and shared sets: answers, codes, the server's keys, a RESET
- RealmGroupTests โ a positionless member held by the sessions of the realm its region is in
- FrameApplierTests โ the client's complete set, release and
RESET, a duplicate completion counted - The
stream-kitchen-sink,tick-blocksandcatalog-refusalsvectors, replayed by the .NET, C and TypeScript SDKs
๐ Related
- See also: Profiles & observers, Sparse Secondary Index
- Rule: SUB-33