Skip to content

Commit 07619e2

Browse files
committed
refactor: extract moderation and mystery-box logic out of RoomGrain (ARCH-02, partial)
The audit's ARCH-02 R5 recommendation was to pull Trading/Moderation/ MysteryBox out into three separate Orleans grains, characterizing them as low-coupling ("the trade has its own lifecycle", "isolated reservation state"). Investigating the actual code before touching it found the opposite: kick/ban mutate live avatar state (AvatarModule.RemoveAvatarFromPlayerAsync), mute status is read in-process on every chat message by RoomChatSystem, and mystery-box sessions are torn down synchronously from the room tick, from an avatar leaving, and from item removal - all of which currently get single-grain-turn atomicity for free. Moving these to separate grains would turn each of those into a cross-grain call with no transaction to back it, in code that grants prizes and enforces moderation. Given that risk, and no way to validate cross-grain consistency behavior without a live multi-silo cluster, this applies the safer half of the same idea instead: Moderation and MysteryBox move out of RoomGrain's own partial-class files into RoomModerationSystem and RoomMysteryBoxSystem, owned classes constructed the same way this codebase already does for RoomChatSystem/RoomWiredSystem. RoomGrain keeps every existing public/internal method as a one-line delegate, so every external call site (PacketHandlers, RoomObjectModule, the wired kick action, the tick loop, avatar-leave handling) is unchanged. This is a pure move - no method's logic changed - and it's the same pattern already proven safe by Chat/Wired, not a new design. This reduces RoomGrain's own file size/complexity and gives these two areas an isolated home to grow in, without gambling on unverified cross-grain consistency for prize-granting and moderation-enforcement code. Trading is the next candidate (largest of the three); full separate-grain extraction remains a legitimate follow-up if someone invests in a state-sync design and integration tests against a real cluster first. All 251 Vortex.Rooms.Tests pass; full solution build stays clean at 0 warnings.
1 parent b72c351 commit 07619e2

5 files changed

Lines changed: 1025 additions & 881 deletions

File tree

Lines changed: 10 additions & 312 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
using System;
21
using System.Threading;
32
using System.Threading.Tasks;
4-
using Microsoft.Extensions.Logging;
53
using Vortex.Primitives.Action;
6-
using Vortex.Primitives.Events;
7-
using Vortex.Primitives.Navigator.Enums;
8-
using Vortex.Primitives.Permissions;
94
using Vortex.Primitives.Players;
10-
using Vortex.Primitives.Rooms.Enums;
115

126
namespace Vortex.Rooms.Grains;
137

@@ -17,333 +11,37 @@ public Task<bool> KickUserAsync(
1711
ActionContext actorCtx,
1812
PlayerId targetPlayerId,
1913
CancellationToken ct
20-
)
21-
{
22-
if (
23-
actorCtx.PlayerId <= 0
24-
|| actorCtx.RoomId != _state.RoomId
25-
|| actorCtx.PlayerId == targetPlayerId
26-
)
27-
{
28-
return Task.FromResult(false);
29-
}
30-
31-
return KickUserGuardedAsync(actorCtx, targetPlayerId, ct);
32-
}
33-
34-
private async Task<bool> KickUserGuardedAsync(
35-
ActionContext actorCtx,
36-
PlayerId targetPlayerId,
37-
CancellationToken ct
38-
)
39-
{
40-
if (
41-
!await CanModerateAsync(
42-
actorCtx,
43-
_state.RoomSnapshot.ModSettings.WhoCanKick,
44-
ModerationAction.Kick
45-
)
46-
)
47-
{
48-
return false;
49-
}
50-
51-
return await KickUserInternalAsync(actorCtx, targetPlayerId, ct).ConfigureAwait(true);
52-
}
53-
54-
/// <summary>
55-
/// Whether <paramref name="actorCtx"/> may perform a moderation action gated by
56-
/// <paramref name="setting"/> in this room. System/wired origins resolve to moderator and pass.
57-
/// </summary>
58-
private async Task<bool> CanModerateAsync(
59-
ActionContext actorCtx,
60-
ModSettingType setting,
61-
ModerationAction action
62-
)
63-
{
64-
RoomControllerType level = await SecurityModule
65-
.GetControllerLevelAsync(actorCtx)
66-
.ConfigureAwait(true);
67-
68-
bool hasStaffCapability = await SecurityModule
69-
.HasStaffModerationCapabilityAsync(actorCtx, action)
70-
.ConfigureAwait(true);
71-
72-
if (RoomModerationPolicy.CanModerate(level, setting, hasStaffCapability))
73-
{
74-
return true;
75-
}
76-
77-
_logger.LogDebug(
78-
"Player {ActorId} lacks {Setting} moderation rights in room {RoomId} (level {Level}).",
79-
actorCtx.PlayerId,
80-
setting,
81-
_state.RoomId,
82-
level
83-
);
84-
85-
return false;
86-
}
14+
) => ModerationSystem.KickUserAsync(actorCtx, targetPlayerId, ct);
8715

8816
/// <summary>Kicks a user without a human actor — for wired / system-driven kicks (the
8917
/// <c>wf_act_kick_user</c> action). Called directly on the grain from inside its own turn, so it is
9018
/// not a re-entrant grain-reference call.</summary>
9119
public Task<bool> KickUserFromWiredAsync(PlayerId targetPlayerId, CancellationToken ct) =>
92-
KickUserInternalAsync(ActionContext.CreateForWired(_state.RoomId), targetPlayerId, ct);
93-
94-
private async Task<bool> KickUserInternalAsync(
95-
ActionContext ctx,
96-
PlayerId targetPlayerId,
97-
CancellationToken ct
98-
)
99-
{
100-
try
101-
{
102-
if (targetPlayerId <= 0 || !_state.AvatarsByPlayerId.ContainsKey(targetPlayerId))
103-
{
104-
return false;
105-
}
106-
107-
await AvatarModule
108-
.RemoveAvatarFromPlayerAsync(ctx, targetPlayerId, ct)
109-
.ConfigureAwait(true);
20+
ModerationSystem.KickUserFromWiredAsync(targetPlayerId, ct);
11021

111-
await _events
112-
.PublishAsync(
113-
new PlayerKickedFromRoomEvent(
114-
ctx.PlayerId,
115-
targetPlayerId,
116-
_state.RoomId.Value
117-
),
118-
ct
119-
)
120-
.ConfigureAwait(true);
121-
122-
return true;
123-
}
124-
catch (Exception ex)
125-
{
126-
_logger.LogWarning(
127-
ex,
128-
"Failed to kick player {TargetPlayerId} from room {RoomId}.",
129-
targetPlayerId,
130-
_state.RoomId
131-
);
132-
133-
return false;
134-
}
135-
}
136-
137-
public async Task<bool> MuteUserAsync(
22+
public Task<bool> MuteUserAsync(
13823
ActionContext actorCtx,
13924
PlayerId targetPlayerId,
14025
int durationSeconds,
14126
CancellationToken ct
142-
)
143-
{
144-
if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId)
145-
{
146-
return false;
147-
}
148-
149-
if (durationSeconds <= 0 || actorCtx.PlayerId == targetPlayerId)
150-
{
151-
return false;
152-
}
153-
154-
if (
155-
!await CanModerateAsync(
156-
actorCtx,
157-
_state.RoomSnapshot.ModSettings.WhoCanMute,
158-
ModerationAction.Mute
159-
)
160-
)
161-
{
162-
return false;
163-
}
27+
) => ModerationSystem.MuteUserAsync(actorCtx, targetPlayerId, durationSeconds, ct);
16428

165-
DateTime expiresUtc = DateTime.UtcNow.AddSeconds(durationSeconds);
166-
167-
try
168-
{
169-
await _moderationStore.MuteAsync(_state.RoomId.Value, targetPlayerId, expiresUtc, ct);
170-
_state.MuteExpiresUtc[targetPlayerId] = expiresUtc;
171-
172-
await _events
173-
.PublishAsync(
174-
new PlayerMutedInRoomEvent(
175-
actorCtx.PlayerId,
176-
targetPlayerId,
177-
_state.RoomId.Value,
178-
durationSeconds
179-
),
180-
ct
181-
)
182-
.ConfigureAwait(true);
183-
184-
return true;
185-
}
186-
catch (Exception ex)
187-
{
188-
_logger.LogWarning(
189-
ex,
190-
"Failed to mute player {TargetPlayerId} in room {RoomId}.",
191-
targetPlayerId,
192-
_state.RoomId
193-
);
194-
}
195-
196-
return false;
197-
}
198-
199-
public async Task<bool> BanUserAsync(
29+
public Task<bool> BanUserAsync(
20030
ActionContext actorCtx,
20131
PlayerId targetPlayerId,
20232
int durationSeconds,
20333
CancellationToken ct
204-
)
205-
{
206-
try
207-
{
208-
if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId)
209-
{
210-
return false;
211-
}
212-
213-
if (durationSeconds <= 0 || actorCtx.PlayerId == targetPlayerId)
214-
{
215-
return false;
216-
}
217-
218-
if (
219-
!await CanModerateAsync(
220-
actorCtx,
221-
_state.RoomSnapshot.ModSettings.WhoCanBan,
222-
ModerationAction.Ban
223-
)
224-
)
225-
{
226-
return false;
227-
}
228-
229-
DateTime expiresUtc = DateTime.UtcNow.AddSeconds(durationSeconds);
230-
231-
await _moderationStore.BanAsync(_state.RoomId.Value, targetPlayerId, expiresUtc, ct);
232-
await AvatarModule
233-
.RemoveAvatarFromPlayerAsync(actorCtx, targetPlayerId, ct)
234-
.ConfigureAwait(true);
235-
236-
await _events
237-
.PublishAsync(
238-
new PlayerBannedInRoomEvent(
239-
actorCtx.PlayerId,
240-
targetPlayerId,
241-
_state.RoomId.Value,
242-
durationSeconds
243-
),
244-
ct
245-
)
246-
.ConfigureAwait(true);
34+
) => ModerationSystem.BanUserAsync(actorCtx, targetPlayerId, durationSeconds, ct);
24735

248-
return true;
249-
}
250-
catch (Exception ex)
251-
{
252-
_logger.LogWarning(
253-
ex,
254-
"Failed to ban player {TargetPlayerId} in room {RoomId}.",
255-
targetPlayerId,
256-
_state.RoomId
257-
);
258-
259-
return false;
260-
}
261-
}
262-
263-
public async Task<bool> UnmuteUserAsync(
36+
public Task<bool> UnmuteUserAsync(
26437
ActionContext actorCtx,
26538
PlayerId targetPlayerId,
26639
CancellationToken ct
267-
)
268-
{
269-
try
270-
{
271-
if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId)
272-
{
273-
return false;
274-
}
275-
276-
if (
277-
!await CanModerateAsync(
278-
actorCtx,
279-
_state.RoomSnapshot.ModSettings.WhoCanMute,
280-
ModerationAction.Mute
281-
)
282-
)
283-
{
284-
return false;
285-
}
286-
287-
await _moderationStore
288-
.UnmuteAsync(_state.RoomId.Value, targetPlayerId, ct)
289-
.ConfigureAwait(true);
290-
_state.MuteExpiresUtc.Remove(targetPlayerId);
291-
292-
return true;
293-
}
294-
catch (Exception ex)
295-
{
296-
_logger.LogWarning(
297-
ex,
298-
"Failed to unmute player {TargetPlayerId} in room {RoomId}.",
299-
targetPlayerId,
300-
_state.RoomId
301-
);
40+
) => ModerationSystem.UnmuteUserAsync(actorCtx, targetPlayerId, ct);
30241

303-
return false;
304-
}
305-
}
306-
307-
public async Task<bool> UnbanUserAsync(
42+
public Task<bool> UnbanUserAsync(
30843
ActionContext actorCtx,
30944
PlayerId targetPlayerId,
31045
CancellationToken ct
311-
)
312-
{
313-
try
314-
{
315-
if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId)
316-
{
317-
return false;
318-
}
319-
320-
if (
321-
!await CanModerateAsync(
322-
actorCtx,
323-
_state.RoomSnapshot.ModSettings.WhoCanBan,
324-
ModerationAction.Ban
325-
)
326-
)
327-
{
328-
return false;
329-
}
330-
331-
await _moderationStore
332-
.UnbanAsync(_state.RoomId.Value, targetPlayerId, ct)
333-
.ConfigureAwait(true);
334-
335-
return true;
336-
}
337-
catch (Exception ex)
338-
{
339-
_logger.LogWarning(
340-
ex,
341-
"Failed to unban player {TargetPlayerId} in room {RoomId}.",
342-
targetPlayerId,
343-
_state.RoomId
344-
);
345-
346-
return false;
347-
}
348-
}
46+
) => ModerationSystem.UnbanUserAsync(actorCtx, targetPlayerId, ct);
34947
}

0 commit comments

Comments
 (0)