diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index fc17597..25c8752 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -4778,6 +4778,15 @@ namespace Oxide.Plugins break; case PermOpKind.EnsureMember: + // Before the read, because on Carbon the read is the thing that lies: a + // never-seen player's UserHasGroup reads the shared blank record (see + // HasNoCarbonRecord), so it can say "already a member" of nobody. + if (HasNoCarbonRecord(op.Subject)) + { + job.Pending.Add(op.Subject + ":" + op.Object); + break; + } + if (permission.UserHasGroup(op.Subject, op.Object)) { job.AlreadyCorrect++; @@ -4800,6 +4809,15 @@ namespace Oxide.Plugins if (!Registered(job, op.Object)) break; + // Never-seen on Carbon: the grant would land in the shared blank record and be + // held by every Steam id Carbon has no record for. It lands at the first sync + // after they join, as PLAN.md §29.8 already says of Carbon. + if (HasNoCarbonRecord(op.Subject)) + { + job.NotLanded.Add(op.Subject + ":" + op.Object); + break; + } + if (HoldsDirectly(op.Subject, op.Object)) { job.AlreadyCorrect++; @@ -4942,6 +4960,24 @@ namespace Oxide.Plugins return false; } + /// + /// Whether this is Carbon and Carbon has no user record for the player. + /// + /// + /// Carbon (2.0.262) answers GetUserData for a Steam id it has never seen with ONE + /// shared, static blank record, and AddUserGroup and GrantUserPermission + /// write into whatever it returns. So a membership or grant for one never-seen player + /// goes to that shared record and is then held by every Steam id Carbon has no record + /// for, until the server restarts — and UserHasGroup reads it back as a success. + /// Oxide creates a real record instead, so this is never true there and Oxide's + /// behaviour is unchanged. Found in the account deletion walk (W3). + /// + /// + private bool HasNoCarbonRecord(string steamId) + { + return IsCarbon() && !permission.UserExists(steamId); + } + /// /// Whether a player holds a permission through a grant of their own, as opposed to /// through a group.