mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,938 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Concurrent;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Accounts.Local;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Events;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
|
||||
namespace RadicadeServer.Utils
|
||||
{
|
||||
public static class MatchmakingUtils
|
||||
{
|
||||
public static IDbContextFactory<RecDBContext> DbFactory = null!;
|
||||
private static readonly ConcurrentDictionary<long, byte> InRoomSessions = new();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static void MarkInRoom(long playerId) => InRoomSessions[playerId] = 0;
|
||||
public static void ClearInRoom(long playerId) => InRoomSessions.TryRemove(playerId, out _);
|
||||
|
||||
public static RoomInstance NormalizeRoomInstance(RoomInstance roomInstance)
|
||||
{
|
||||
roomInstance.eventId ??= 0;
|
||||
roomInstance.clubId ??= 0;
|
||||
roomInstance.roomCode ??= string.Empty;
|
||||
roomInstance.location ??= string.Empty;
|
||||
roomInstance.name ??= string.Empty;
|
||||
roomInstance.photonRegion ??= string.Empty;
|
||||
roomInstance.photonRegionId ??= string.Empty;
|
||||
roomInstance.photonRoomId ??= string.Empty;
|
||||
roomInstance.dataBlob ??= string.Empty;
|
||||
|
||||
return roomInstance;
|
||||
}
|
||||
|
||||
public static async Task<RoomInstance?> EnsureDormPresenceAsync(
|
||||
RecDBContext db,
|
||||
long playerId,
|
||||
RealPresence presence)
|
||||
{
|
||||
FullRoom? dorm = await RoomUtils.GetOrCreateDorm(playerId);
|
||||
if (dorm == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RoomInstance? dormInstance = null;
|
||||
if (presence.roomInstance.HasValue)
|
||||
{
|
||||
dormInstance = await db.RoomInstances.FirstOrDefaultAsync(x =>
|
||||
x.roomInstanceId == presence.roomInstance.Value &&
|
||||
x.roomId == dorm.RoomId &&
|
||||
x.isPrivate);
|
||||
}
|
||||
|
||||
if (dormInstance == null)
|
||||
{
|
||||
dormInstance = await CreateRoomInstance(
|
||||
dorm.RoomId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
if (dormInstance == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isNewDormVisit = !presence.isOnline || presence.roomInstance != dormInstance.roomInstanceId;
|
||||
presence.roomInstance = dormInstance.roomInstanceId;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
presence.isOnline = true;
|
||||
MarkInRoom(playerId);
|
||||
|
||||
if (isNewDormVisit)
|
||||
await RecordRoomVisit(db, playerId, dorm.RoomId);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NormalizeRoomInstance(dormInstance);
|
||||
}
|
||||
|
||||
public static async Task<RoomInstance?> TryStartOrientationAsync(
|
||||
RecDBContext db,
|
||||
long playerId,
|
||||
RealPresence presence)
|
||||
{
|
||||
if (!await NeedsOrientationAsync(db, playerId))
|
||||
return null;
|
||||
|
||||
FullRoom? orientation = await RoomUtils.GetRoom("Orientation");
|
||||
if (orientation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RoomInstance? instance = await CreateRoomInstance(
|
||||
orientation.RoomId,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await ClearNeedsOrientationAsync(db, playerId);
|
||||
|
||||
presence.roomInstance = instance.roomInstanceId;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
presence.isOnline = true;
|
||||
MarkInRoom(playerId);
|
||||
|
||||
await RecordRoomVisit(db, playerId, orientation.RoomId);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NormalizeRoomInstance(instance);
|
||||
}
|
||||
|
||||
public static async Task<bool> IsPlayerInAnyInstances(long playerId)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var presence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId);
|
||||
if (presence?.roomInstance == null) return false;
|
||||
|
||||
var instance = await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance);
|
||||
return instance != null;
|
||||
}
|
||||
|
||||
public static async Task<MatchResponse> Matchmake(
|
||||
JoinMode joinMode,
|
||||
string roomName,
|
||||
long playerId,
|
||||
long? subRoomId = null)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var presence = await ctx.Presences
|
||||
.FirstOrDefaultAsync(x => x.playerId == playerId);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.UnknownError);
|
||||
}
|
||||
|
||||
roomName = NormalizeRoomName(roomName);
|
||||
|
||||
if (string.Equals(roomName, "none", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
presence.roomInstance = null;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
ClearInRoom(playerId);
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.Success,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
var profile = await ctx.Profiles
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.accountId == playerId);
|
||||
|
||||
string? ownDormName = profile == null
|
||||
? null
|
||||
: $"@{profile.username}'s Dorm";
|
||||
|
||||
bool isOwnDormRequest =
|
||||
string.Equals(roomName, "DormRoom", StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrWhiteSpace(ownDormName) &&
|
||||
string.Equals(roomName, ownDormName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync(ctx, playerId, isOwnDormRequest, $"room '{roomName}'");
|
||||
if (warningBounce != null) return warningBounce;
|
||||
|
||||
bool isBanned = await ctx.IsBanned(playerId);
|
||||
|
||||
if (isBanned && !isOwnDormRequest)
|
||||
{
|
||||
var requestedRoom = await RoomUtils.GetRoom(roomName);
|
||||
|
||||
bool isOwnedDorm =
|
||||
requestedRoom != null &&
|
||||
requestedRoom.IsDorm &&
|
||||
requestedRoom.CreatorAccountId == playerId;
|
||||
|
||||
if (!isOwnedDorm)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.Banned,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
FullRoom? room;
|
||||
|
||||
if (isOwnDormRequest)
|
||||
{
|
||||
RoomInstance? orientationInstance = await TryStartOrientationAsync(ctx, playerId, presence);
|
||||
if (orientationInstance != null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.Success,
|
||||
roomInstance = orientationInstance
|
||||
};
|
||||
}
|
||||
|
||||
room = await RoomUtils.GetOrCreateDorm(playerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
room = await RoomUtils.GetRoom(roomName);
|
||||
}
|
||||
|
||||
if (room == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.RoomIsNotActive,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (await IsBannedFromRoom(ctx, playerId, room.RoomId))
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.BannedFromRoom,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
if (room.Accessibility == Accessibility.Private &&
|
||||
!await IsAtLeastRole(playerId, room.RoomId, RoleType.CoOwner))
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.RoomIsPrivate);
|
||||
}
|
||||
|
||||
if (presence.roomInstance.HasValue)
|
||||
{
|
||||
await RemovePresence(playerId);
|
||||
}
|
||||
|
||||
RoomInstance? newInstance = null;
|
||||
|
||||
if (isOwnDormRequest || joinMode == JoinMode.PrivateNewInstance)
|
||||
{
|
||||
newInstance = await CreateRoomInstance(
|
||||
room.RoomId,
|
||||
true,
|
||||
subRoomId,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (newInstance == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.UnknownError,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (newInstance != null)
|
||||
{
|
||||
return await JoinRoom(playerId, newInstance.roomInstanceId);
|
||||
}
|
||||
|
||||
await ReapEmptyPublicInstancesAsync(ctx, room.RoomId, subRoomId);
|
||||
|
||||
var query = ctx.RoomInstances.Where(i =>
|
||||
!i.isFull &&
|
||||
i.roomId == room.RoomId &&
|
||||
!i.isPrivate &&
|
||||
(!subRoomId.HasValue || i.subRoomId == subRoomId));
|
||||
|
||||
int count = await query.CountAsync();
|
||||
RoomInstance? publicInstance = null;
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
int skip = Random.Shared.Next(0, count);
|
||||
publicInstance = await query.Skip(skip).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
publicInstance ??= await CreateRoomInstance(
|
||||
room.RoomId,
|
||||
false,
|
||||
subRoomId,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (publicInstance == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.UnknownError,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
return await JoinRoom(playerId, publicInstance.roomInstanceId);
|
||||
}
|
||||
|
||||
public static async Task<MatchResponse> MatchmakeEvent(long playerId, long eventId,
|
||||
JoinMode joinMode = JoinMode.PublicMatchmaking)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var presence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId);
|
||||
if (presence == null)
|
||||
return MatchResponse.Error(JoinStatus.UnknownError);
|
||||
|
||||
var ev = await ctx.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId);
|
||||
if (ev == null)
|
||||
return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (ev.StartTime > now && ev.CreatorPlayerId != playerId)
|
||||
return MatchResponse.Error(JoinStatus.EventNotStarted);
|
||||
|
||||
|
||||
if (ev.EndTime <= now)
|
||||
return MatchResponse.Error(JoinStatus.EventAlreadyFinished);
|
||||
|
||||
var room = await RoomUtils.GetRoom(ev.RoomId);
|
||||
if (room == null)
|
||||
return MatchResponse.Error(JoinStatus.RoomIsNotActive);
|
||||
|
||||
if (await ctx.IsBanned(playerId))
|
||||
return MatchResponse.Error(JoinStatus.Banned);
|
||||
|
||||
if (await IsBannedFromRoom(ctx, playerId, ev.RoomId))
|
||||
return MatchResponse.Error(JoinStatus.BannedFromRoom);
|
||||
|
||||
var eventInvitation = await ctx.Messages.AsNoTracking()
|
||||
.AnyAsync(message => message.ToPlayerId == playerId &&
|
||||
message.Type == MessageType.PlayerEventInvitation &&
|
||||
message.PlayerEventId == eventId);
|
||||
var eventResponse = await ctx.PlayerEventResponses.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.PlayerEventId == eventId && x.PlayerId == playerId);
|
||||
var hasAccepted = eventResponse?.Type == EventResponses.Yes;
|
||||
var hasDeclined = eventResponse?.Type == EventResponses.No;
|
||||
var hasPendingInvitation = eventResponse?.Type == EventResponses.Pending;
|
||||
var canJoinEvent = ev.CreatorPlayerId == playerId || hasAccepted ||
|
||||
((eventInvitation || hasPendingInvitation) && !hasDeclined);
|
||||
|
||||
if (ev.Accessibility == Accessibility.Private && !canJoinEvent)
|
||||
return MatchResponse.Error(JoinStatus.EventIsPrivate);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (joinMode == JoinMode.PrivateNewInstance)
|
||||
{
|
||||
|
||||
|
||||
var mayUsePrivateRoom = canJoinEvent ||
|
||||
await IsAtLeastRole(playerId, ev.RoomId, RoleType.CoOwner);
|
||||
if (room.Accessibility == Accessibility.Private && !mayUsePrivateRoom)
|
||||
return MatchResponse.Error(JoinStatus.RoomIsPrivate);
|
||||
|
||||
var ownInstance = await CreateRoomInstance(ev.RoomId, true, ev.SubRoomId, null, eventId);
|
||||
if (ownInstance == null)
|
||||
return MatchResponse.Error(JoinStatus.UnknownError);
|
||||
|
||||
return await JoinRoom(playerId, ownInstance.roomInstanceId, canJoinEvent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var broadcastInstanceId = ev.BroadcastingRoomInstanceId ?? 0;
|
||||
var hostInstanceId = 0L;
|
||||
if (broadcastInstanceId > 0)
|
||||
{
|
||||
var hostPresence = await ctx.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.playerId == ev.CreatorPlayerId);
|
||||
hostInstanceId = hostPresence?.roomInstance ?? 0;
|
||||
}
|
||||
|
||||
RoomInstance? instance = null;
|
||||
if (broadcastInstanceId > 0 && ev.CreatorPlayerId == playerId)
|
||||
{
|
||||
instance = await ctx.RoomInstances.FirstOrDefaultAsync(i =>
|
||||
i.roomInstanceId == broadcastInstanceId && !i.isFull)
|
||||
?? await ctx.RoomInstances.FirstOrDefaultAsync(i =>
|
||||
i.roomInstanceId == hostInstanceId && !i.isFull);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
instance ??= await ctx.RoomInstances
|
||||
.FirstOrDefaultAsync(i => i.eventId == eventId && !i.isFull &&
|
||||
!i.isPrivate &&
|
||||
i.roomInstanceId != broadcastInstanceId &&
|
||||
i.roomInstanceId != hostInstanceId);
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
instance = await CreateRoomInstance(ev.RoomId, false, ev.SubRoomId, null, eventId);
|
||||
if (instance == null)
|
||||
return MatchResponse.Error(JoinStatus.UnknownError);
|
||||
}
|
||||
|
||||
return await JoinRoom(playerId, instance.roomInstanceId, canJoinEvent);
|
||||
}
|
||||
|
||||
public static async Task<MatchResponse> JoinRoom(long playerId, long instanceId, bool allowPrivateRoom = false)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
if (instanceId < 0) return new MatchResponse { errorCode = JoinStatus.NoSuchRoom };
|
||||
|
||||
var roomInstance = await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId);
|
||||
var playerPresence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId);
|
||||
if (playerPresence == null) return new MatchResponse { roomInstance = null, errorCode = JoinStatus.UnknownError };
|
||||
if (roomInstance == null) return new MatchResponse { roomInstance = null, errorCode = JoinStatus.NoSuchRoom };
|
||||
|
||||
var room = await ctx.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId);
|
||||
if (room == null) return new MatchResponse { roomInstance = null, errorCode = JoinStatus.NoSuchRoom };
|
||||
|
||||
|
||||
|
||||
if (await IsBannedFromRoom(ctx, playerId, room.RoomId))
|
||||
return new MatchResponse { roomInstance = null, errorCode = JoinStatus.BannedFromRoom };
|
||||
|
||||
var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync(
|
||||
ctx, playerId, room.IsDorm && room.CreatorAccountId == playerId, $"instance {instanceId} (room {room.RoomId})");
|
||||
if (warningBounce != null) return warningBounce;
|
||||
|
||||
if (room.Accessibility == Accessibility.Private &&
|
||||
!allowPrivateRoom &&
|
||||
!await IsAtLeastRole(playerId, room.RoomId, RoleType.CoOwner))
|
||||
{
|
||||
return new MatchResponse { roomInstance = null, errorCode = JoinStatus.RoomIsPrivate };
|
||||
}
|
||||
|
||||
var playersInInstance = await GetPlayerCountInInstance(instanceId);
|
||||
if (playersInInstance >= roomInstance.maxCapacity)
|
||||
return new MatchResponse { roomInstance = null, errorCode = JoinStatus.InsufficientSpace };
|
||||
|
||||
playerPresence.roomInstance = instanceId;
|
||||
playerPresence.isOnline = true;
|
||||
playerPresence.errorCode = JoinStatus.Success;
|
||||
|
||||
|
||||
|
||||
if (roomInstance.eventId is > 0)
|
||||
roomInstance.roomInstanceType = await ctx.PlayerEvents.AnyAsync(ev =>
|
||||
ev.PlayerEventId == roomInstance.eventId && ev.IsMultiInstance)
|
||||
? RoomInstanceType.MultiInstanceEvent : RoomInstanceType.Event;
|
||||
|
||||
await RecordRoomVisit(ctx, playerId, room.RoomId);
|
||||
await RoomJoinGiftUtils.TryDeliverAsync(ctx, playerId, room);
|
||||
|
||||
playersInInstance = await GetPlayerCountInInstance(instanceId);
|
||||
if (playersInInstance+1 >= roomInstance.maxCapacity)
|
||||
{
|
||||
roomInstance.isFull = true;
|
||||
ctx.RoomInstances.Update(roomInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (roomInstance.isFull)
|
||||
{
|
||||
roomInstance.isFull = false;
|
||||
ctx.RoomInstances.Update(roomInstance);
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
MarkInRoom(playerId);
|
||||
|
||||
|
||||
|
||||
|
||||
var hostedEvent = await ctx.PlayerEvents.FirstOrDefaultAsync(ev =>
|
||||
ev.PlayerEventId == roomInstance.eventId && ev.CreatorPlayerId == playerId &&
|
||||
ev.BroadcastingRoomInstanceId != null && ev.BroadcastingRoomInstanceId != instanceId);
|
||||
if (hostedEvent != null && !await ctx.Presences.AnyAsync(p =>
|
||||
p.isOnline && p.roomInstance == hostedEvent.BroadcastingRoomInstanceId))
|
||||
{
|
||||
hostedEvent.BroadcastingRoomInstanceId = null;
|
||||
await ctx.SaveChangesAsync();
|
||||
await RadicadeServer.RecNet.Events.PlayerEventNotifications.PushToEventInstancesAsync(ctx, hostedEvent);
|
||||
}
|
||||
|
||||
return new MatchResponse { roomInstance = roomInstance, errorCode = JoinStatus.Success };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task<string?> GetLastVisitedRoomNameAsync(RecDBContext ctx, long playerId)
|
||||
{
|
||||
var visit = await ctx.RoomInteractions
|
||||
.Where(x => x.PlayerId == playerId)
|
||||
.OrderByDescending(x => x.LastVisted)
|
||||
.FirstOrDefaultAsync();
|
||||
if (visit == null) return null;
|
||||
|
||||
return await ctx.Rooms
|
||||
.Where(x => x.RoomId == visit.RoomId)
|
||||
.Select(x => x.Name)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public static async Task<string?> GetLastRewardActivityRoomNameAsync(
|
||||
RecDBContext ctx,
|
||||
long playerId,
|
||||
TimeSpan window)
|
||||
{
|
||||
var since = DateTime.Now - window;
|
||||
|
||||
var recent = await ctx.RoomInteractions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.PlayerId == playerId && x.LastVisted >= since)
|
||||
.OrderByDescending(x => x.LastVisted)
|
||||
.Take(VisitsConsidered)
|
||||
.Select(x => x.RoomId)
|
||||
.ToListAsync();
|
||||
|
||||
if (recent.Count == 0) return null;
|
||||
|
||||
var names = await ctx.Rooms
|
||||
.AsNoTracking()
|
||||
.Where(x => recent.Contains(x.RoomId))
|
||||
.Select(x => new { x.RoomId, x.Name })
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var roomId in recent)
|
||||
{
|
||||
var name = names.FirstOrDefault(x => x.RoomId == roomId)?.Name;
|
||||
if (RroRewardPools.IsRewardActivity(name)) return name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private const int VisitsConsidered = 25;
|
||||
|
||||
public static async Task RecordRoomVisit(RecDBContext ctx, long playerId, long roomId)
|
||||
{
|
||||
var existingInteraction = await ctx.RoomInteractions.FirstOrDefaultAsync(x => x.RoomId == roomId && x.PlayerId == playerId);
|
||||
bool isNewVisitor = existingInteraction == null || !existingInteraction.Visited;
|
||||
if (existingInteraction != null)
|
||||
{
|
||||
existingInteraction.Visited = true;
|
||||
existingInteraction.VisitCount++;
|
||||
existingInteraction.LastVisted = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
await ctx.AddAsync(new PlayerInteractionDB { Cheered = false, Favorited = false, PlayerId = playerId, RoomId = roomId, VisitCount = 1, Visited = true });
|
||||
}
|
||||
|
||||
|
||||
var roomStats = await ctx.RoomStats.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (roomStats == null)
|
||||
{
|
||||
roomStats = new RoomStats { RoomId = roomId };
|
||||
await ctx.AddAsync(roomStats);
|
||||
}
|
||||
roomStats.VisitCount++;
|
||||
if (isNewVisitor)
|
||||
{
|
||||
roomStats.VisitorCount++;
|
||||
}
|
||||
|
||||
await QuestRewardDelivery.DeliverOnArrivalAsync(ctx, playerId, roomId);
|
||||
}
|
||||
|
||||
public static async Task<RoomInstance?> CreateRoomInstance(FullRoom room, bool isPrivate, long? subRoomId, long? clubId, long? eventId)
|
||||
{
|
||||
return await CreateRoomInstance(room.RoomId, isPrivate, subRoomId, clubId, eventId);
|
||||
}
|
||||
|
||||
public static async Task<RoomInstance?> CreateRoomInstance(
|
||||
long roomId, bool isPrivate, long? subRoomId, long? clubId, long? eventId)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
SubRoom? subRoom = null;
|
||||
FullRoom? room = await RoomUtils.GetRoom(roomId);
|
||||
RoomInstance? roomInstance = null;
|
||||
|
||||
if (room == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (subRoomId == null)
|
||||
{
|
||||
subRoom = await ctx.SubRooms.FirstOrDefaultAsync(x => x.RoomId == room.RoomId);
|
||||
}
|
||||
else
|
||||
{
|
||||
subRoom = await ctx.SubRooms.FirstOrDefaultAsync(x => x.RoomId == room.RoomId && x.SubRoomId == subRoomId);
|
||||
}
|
||||
if (subRoom == null) return null;
|
||||
|
||||
|
||||
RoomInstanceType instanceType = RoomInstanceType.Public;
|
||||
|
||||
if (isPrivate) instanceType = RoomInstanceType.Private;
|
||||
if (room.Name == "DormRoom") instanceType = RoomInstanceType.Dormroom;
|
||||
if (clubId != null) instanceType = RoomInstanceType.Clubhouse;
|
||||
if (eventId != null)
|
||||
instanceType = await ctx.PlayerEvents.AnyAsync(ev => ev.PlayerEventId == eventId && ev.IsMultiInstance)
|
||||
? RoomInstanceType.MultiInstanceEvent : RoomInstanceType.Event;
|
||||
|
||||
|
||||
|
||||
string roomName = "^" + room.Name;
|
||||
if (room.IsDorm) {
|
||||
Profile? roomCreator = await ctx.Profiles.AsNoTracking().FirstOrDefaultAsync(p => p.accountId == room.CreatorAccountId);
|
||||
if (roomCreator != null)
|
||||
{
|
||||
roomName = $"@{roomCreator.username}'s Dorm";
|
||||
}
|
||||
}
|
||||
|
||||
roomInstance = new RoomInstance
|
||||
{
|
||||
subRoomId = subRoom.SubRoomId,
|
||||
roomId = roomId,
|
||||
clubId = clubId,
|
||||
eventId = eventId,
|
||||
dataBlob = subRoom.DataBlob,
|
||||
EncryptVoiceChat = room.EncryptVoiceChat,
|
||||
isFull = false,
|
||||
isInProgress = false,
|
||||
isPrivate = (room.Name == "DormRoom" && room.IsDorm) ? true : isPrivate,
|
||||
maxCapacity = subRoom.MaxPlayers,
|
||||
name = roomName,
|
||||
photonRegion = "us",
|
||||
photonRegionId = "us",
|
||||
photonRoomId = CreatePhotonRoomId(),
|
||||
roomCode = "",
|
||||
location = subRoom.UnitySceneId,
|
||||
roomInstanceType = instanceType
|
||||
};
|
||||
|
||||
await ctx.RoomInstances.AddAsync(roomInstance);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return roomInstance;
|
||||
}
|
||||
|
||||
public static async Task<List<RealPresence>> GetPlayersInInstance(long instanceId)
|
||||
{
|
||||
using var ctx = DbFactory.CreateDbContext();
|
||||
|
||||
return await ctx.Presences.Where((p) => p.roomInstance == instanceId).ToListAsync();
|
||||
}
|
||||
|
||||
public static async Task<int> GetPlayerCountInInstance(long instanceId)
|
||||
{
|
||||
using var ctx = DbFactory.CreateDbContext();
|
||||
|
||||
return await ctx.Presences.AsNoTracking().Where(x => x.roomInstance == instanceId).CountAsync();
|
||||
}
|
||||
|
||||
public static async Task<int> ReapEmptyPublicInstancesAsync(RecDBContext ctx, long roomId, long? subRoomId)
|
||||
{
|
||||
var candidates = await ctx.RoomInstances
|
||||
.Where(i =>
|
||||
i.roomId == roomId &&
|
||||
!i.isPrivate &&
|
||||
i.roomInstanceType == RoomInstanceType.Public &&
|
||||
(!subRoomId.HasValue || i.subRoomId == subRoomId))
|
||||
.ToListAsync();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ids = candidates.Select(x => x.roomInstanceId).ToList();
|
||||
|
||||
var occupied = await ctx.Presences
|
||||
.Where(p => p.isOnline &&
|
||||
p.roomInstance != null &&
|
||||
ids.Contains(p.roomInstance.Value))
|
||||
.Select(p => p.roomInstance!.Value)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var empty = candidates
|
||||
.Where(x => !occupied.Contains(x.roomInstanceId))
|
||||
.ToList();
|
||||
|
||||
if (empty.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ctx.RoomInstances.RemoveRange(empty);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return empty.Count;
|
||||
}
|
||||
|
||||
public static async Task RemovePresence(long playerId)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
|
||||
var player = ctx.Presences.FirstOrDefault((x) => x.playerId == playerId);
|
||||
if (player?.roomInstance == null) return;
|
||||
|
||||
|
||||
|
||||
long roomInstanceId = player.roomInstance.Value;
|
||||
player.roomInstance = null;
|
||||
|
||||
var remainingPlayers = await GetPlayerCountInInstance(roomInstanceId);
|
||||
|
||||
if (remainingPlayers <= 0)
|
||||
{
|
||||
var roomInstance = await ctx.RoomInstances.FirstOrDefaultAsync((x) => x.roomInstanceId == roomInstanceId);
|
||||
if (roomInstance != null)
|
||||
ctx.RoomInstances.Remove(roomInstance);
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static async Task<bool> IsAtLeastRole(long playerId, long roomId, RoleType minimumRole)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var role = await RoomUtils.GetRoleForPlayer(playerId, roomId);
|
||||
var isOwner = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(r => r.CreatorAccountId == playerId && r.RoomId == roomId) != null;
|
||||
|
||||
return (role?.Role >= minimumRole) || isOwner;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task<bool> CanModerateRoom(long playerId, long roomId)
|
||||
{
|
||||
if (await IsAtLeastRole(playerId, roomId, RoleType.Moderator)) return true;
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
var highRanks = (await ctx.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks"))?.Value;
|
||||
if (string.IsNullOrWhiteSpace(highRanks)) return false;
|
||||
|
||||
foreach (var entry in highRanks.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
if (long.TryParse(entry, out var highRankId) && highRankId == playerId)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task<bool> IsBannedFromRoom(RecDBContext ctx, long playerId, long roomId)
|
||||
{
|
||||
var ban = await ctx.RoomBans.AsNoTracking().FirstOrDefaultAsync(x =>
|
||||
x.roomId == roomId && x.bannedPlayerId == playerId);
|
||||
|
||||
if (ban == null) return false;
|
||||
|
||||
|
||||
if (ban.expiresAt.HasValue && ban.expiresAt.Value <= DateTime.UtcNow)
|
||||
{
|
||||
ctx.RoomBans.Remove(ban);
|
||||
await ctx.SaveChangesAsync();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await IsAtLeastRole(playerId, roomId, RoleType.CoOwner)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task KickPlayerFromRoom(long playerId, long roomId, string reason)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var presence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == playerId);
|
||||
if (presence?.roomInstance == null) return;
|
||||
|
||||
var instance = await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance);
|
||||
if (instance == null || instance.roomId != roomId) return;
|
||||
|
||||
var dorm = await RoomUtils.GetOrCreateDorm(playerId);
|
||||
if (dorm == null) return;
|
||||
var dormInstance = await CreateRoomInstance(dorm.RoomId, true, null, null, null);
|
||||
if (dormInstance == null) return;
|
||||
|
||||
presence.roomInstance = dormInstance.roomInstanceId;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
presence.isOnline = true;
|
||||
MarkInRoom(playerId);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await Notify.SendNotify(playerId, new Notification { Id = NotificationType.ModerationKick, Msg = reason });
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
try
|
||||
{
|
||||
await Notify.SendNotify(playerId, new Notification { Id = "PresenceUpdate", Msg = await Presence.CreateAsync(ctx, presence, dormInstance) });
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
|
||||
private static async Task<bool> NeedsOrientationAsync(RecDBContext ctx, long playerId)
|
||||
{
|
||||
var settings = await ctx.PlayerSettings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.AccountId == playerId);
|
||||
if (settings == null) return false;
|
||||
|
||||
var list = settings.DeserializeSettings();
|
||||
var marker = list.FirstOrDefault(x => x.Key == "Radicade.NeedsOrientation");
|
||||
return marker != null &&
|
||||
(marker.Value == "1" || string.Equals(marker.Value, "true", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static async Task ClearNeedsOrientationAsync(RecDBContext ctx, long playerId)
|
||||
{
|
||||
var settings = await ctx.PlayerSettings
|
||||
.FirstOrDefaultAsync(x => x.AccountId == playerId);
|
||||
if (settings == null) return;
|
||||
|
||||
var list = settings.DeserializeSettings();
|
||||
if (list.RemoveAll(x => x.Key == "Radicade.NeedsOrientation") > 0)
|
||||
{
|
||||
settings.Settings = list.ToJson();
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeRoomName(string roomName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roomName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string normalized = roomName.Replace("+", " ").Trim();
|
||||
|
||||
|
||||
|
||||
|
||||
if (normalized.StartsWith("^", StringComparison.Ordinal))
|
||||
{
|
||||
normalized = normalized[1..].TrimStart();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
normalized = Uri.UnescapeDataString(normalized);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static string CreatePhotonRoomId()
|
||||
{
|
||||
return $"Radicade{Random.Shared.Next(232)}-{Guid.NewGuid().ToString().Replace("-", "")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user