mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
|
||||
namespace RadicadeServer.RecNet.Matchmaking
|
||||
{
|
||||
public class GoToRequest
|
||||
{
|
||||
public JoinMode JoinMode { get; set; }
|
||||
public bool BypassMovementModeRestriction { get; set; }
|
||||
public string LoginLock { get; set; } = null!;
|
||||
public long[]? AdditionalPlayerIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1238 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Config;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.API.Storefront;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Matchmaking;
|
||||
using RadicadeServer.RecNet.Messages;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
namespace RadicadeServer.API.Matchmaking
|
||||
{
|
||||
[Route("matchmaking")]
|
||||
public class MatchmakingAPI : Controller
|
||||
{
|
||||
private readonly RecDBContext db;
|
||||
|
||||
public MatchmakingAPI(RecDBContext context)
|
||||
{
|
||||
db = context;
|
||||
}
|
||||
|
||||
[HttpGet("rooms/requiring/developer")]
|
||||
[HttpGet("rooms/requiring/rrplus")]
|
||||
public IActionResult GetRestrictedRooms() => Ok(Array.Empty<object>());
|
||||
|
||||
[HttpPost("roominstance/{instanceId}/reportjoinresult"), Authorize]
|
||||
public async Task<IActionResult> ReportJoinResult(long instanceId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId.HasValue)
|
||||
{
|
||||
var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId);
|
||||
if (roomInstance != null)
|
||||
{
|
||||
await RoomJoinGiftUtils.TryDeliverAsync(db, userId.Value, roomInstance.roomId);
|
||||
}
|
||||
|
||||
await StorefrontShared.SendPendingGiftBoxNotificationAsync(db, userId.Value);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/none"), Authorize]
|
||||
public async Task<IActionResult> MatchmakeNone()
|
||||
{
|
||||
long? userId = User.GetUserId();
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var match = await MatchmakingUtils.Matchmake(
|
||||
JoinMode.PrivateNewInstance,
|
||||
"none",
|
||||
userId.Value);
|
||||
|
||||
return Ok(match);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/dorm"), Authorize]
|
||||
public async Task<IActionResult> MatchmakeDorm()
|
||||
{
|
||||
long? userId = User.GetUserId();
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var presence = await db.Presences
|
||||
.FirstOrDefaultAsync(x => x.playerId == userId.Value);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
presence = new RealPresence { playerId = userId.Value };
|
||||
db.Presences.Add(presence);
|
||||
}
|
||||
|
||||
var orientationInstance = await MatchmakingUtils.TryStartOrientationAsync(db, userId.Value, presence);
|
||||
if (orientationInstance != null)
|
||||
{
|
||||
await Notify.UpdateSubscriptions(userId.Value);
|
||||
return Ok(new MatchResponse
|
||||
{
|
||||
roomInstance = orientationInstance,
|
||||
errorCode = JoinStatus.Success
|
||||
});
|
||||
}
|
||||
|
||||
var dormInstance = await MatchmakingUtils.EnsureDormPresenceAsync(db, userId.Value, presence);
|
||||
|
||||
if (dormInstance == null)
|
||||
{
|
||||
var reloaded = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId.Value);
|
||||
if (reloaded != null)
|
||||
{
|
||||
reloaded.roomInstance = null;
|
||||
await db.SaveChangesAsync();
|
||||
dormInstance = await MatchmakingUtils.EnsureDormPresenceAsync(db, userId.Value, reloaded);
|
||||
}
|
||||
}
|
||||
|
||||
if (dormInstance == null)
|
||||
{
|
||||
return NotFound(new MatchResponse { errorCode = JoinStatus.BlockedFromRoom });
|
||||
}
|
||||
|
||||
await Notify.UpdateSubscriptions(userId.Value);
|
||||
return Ok(new MatchResponse
|
||||
{
|
||||
roomInstance = dormInstance,
|
||||
errorCode = JoinStatus.Success
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
new MatchResponse { errorCode = JoinStatus.BlockedFromRoom });
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<RoomInstance?> ResolvePresenceRoomInstanceAsync(RealPresence data)
|
||||
{
|
||||
if (data.roomInstance is not > 0) return null;
|
||||
|
||||
var roomInstance = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.roomInstanceId == data.roomInstance.Value);
|
||||
|
||||
return roomInstance == null ? null : MatchmakingUtils.NormalizeRoomInstance(roomInstance);
|
||||
}
|
||||
|
||||
[HttpGet("player")]
|
||||
public async Task<List<Presence>> GetPlayerPresence(
|
||||
[FromQuery] List<long> id)
|
||||
{
|
||||
var presences = new List<Presence>();
|
||||
|
||||
if (id == null || id.Count == 0)
|
||||
{
|
||||
long? currentPlayerId = User.GetUserId();
|
||||
|
||||
if (!currentPlayerId.HasValue)
|
||||
{
|
||||
return presences;
|
||||
}
|
||||
|
||||
id = new List<long>
|
||||
{
|
||||
currentPlayerId.Value
|
||||
};
|
||||
}
|
||||
|
||||
foreach (long playerId in id)
|
||||
{
|
||||
RealPresence? data = await db.Presences
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.playerId == playerId
|
||||
);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
long? viewerId = User.GetUserId();
|
||||
|
||||
RoomInstance? roomInstance = await ResolvePresenceRoomInstanceAsync(data);
|
||||
|
||||
if (viewerId != playerId)
|
||||
roomInstance = await RoomUtils.ForViewerAsync(roomInstance, viewerId);
|
||||
|
||||
presences.Add(
|
||||
(await Presence.CreateAsync(db,
|
||||
data,
|
||||
roomInstance
|
||||
)).ForViewer(viewerId)
|
||||
);
|
||||
}
|
||||
|
||||
return presences;
|
||||
}
|
||||
|
||||
[HttpGet("room/{roomId}/instances"), Authorize]
|
||||
public async Task<List<object>?>
|
||||
GetRoomInstancesInRoom(long roomId)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return new List<object>();
|
||||
}
|
||||
|
||||
var roomInstances = await db.RoomInstances
|
||||
.Where(x => x.roomId == roomId)
|
||||
.ToListAsync();
|
||||
|
||||
var converted = new List<object>();
|
||||
|
||||
foreach (RoomInstance instance in roomInstances)
|
||||
{
|
||||
var players = await db.Presences
|
||||
.Where(
|
||||
x => x.roomInstance ==
|
||||
instance.roomInstanceId
|
||||
)
|
||||
.Select(x => x.playerId)
|
||||
.ToListAsync();
|
||||
|
||||
var summary = new Dictionary<string, object?>
|
||||
{
|
||||
["roomInstanceId"] = instance.roomInstanceId,
|
||||
["roomId"] = instance.roomId,
|
||||
["subRoomId"] = instance.subRoomId,
|
||||
["isFull"] = instance.isFull,
|
||||
["playerIds"] = players,
|
||||
["createdAt"] = DateTime.UtcNow
|
||||
};
|
||||
|
||||
converted.Add(summary);
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
[HttpGet("clubhousesearch/mostactivenow"), Authorize]
|
||||
public async Task<IActionResult> GetMostActiveClubhouses()
|
||||
{
|
||||
var instances = await db.RoomInstances
|
||||
.Where(x => x.clubId != null && x.clubId > 0)
|
||||
.ToListAsync();
|
||||
|
||||
var byClub = instances.GroupBy(x => x.clubId!.Value);
|
||||
var summaries = new List<object>();
|
||||
|
||||
foreach (var group in byClub)
|
||||
{
|
||||
var playerCount = 0;
|
||||
foreach (var instance in group)
|
||||
{
|
||||
playerCount += await db.Presences
|
||||
.CountAsync(x => x.roomInstance == instance.roomInstanceId);
|
||||
}
|
||||
|
||||
summaries.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["clubId"] = group.Key,
|
||||
["playerCount"] = playerCount,
|
||||
["roomId"] = group.First().roomId
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(summaries.ToArray());
|
||||
}
|
||||
|
||||
[HttpPost("goto/instance/{id}"), Authorize]
|
||||
public async Task<MatchResponse> GoToRoomInstance(
|
||||
long id,
|
||||
[FromForm] long[]? AdditionalPlayerIds)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.Banned);
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
RoomInstance? roomInstance =
|
||||
await db.RoomInstances.FirstOrDefaultAsync(
|
||||
x => x.roomInstanceId == id
|
||||
);
|
||||
|
||||
if (roomInstance == null)
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.RoomIsNotActive);
|
||||
}
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId);
|
||||
if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive);
|
||||
|
||||
bool isOwnDorm = room.IsDorm && room.CreatorAccountId == userId;
|
||||
if (!isOwnDorm && await db.IsBanned(userId))
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.Banned);
|
||||
}
|
||||
|
||||
if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId))
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.BannedFromRoom);
|
||||
}
|
||||
|
||||
var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync(
|
||||
db, userId, isOwnDorm, $"instance {id} (room {roomInstance.roomId})");
|
||||
if (warningBounce != null) return warningBounce;
|
||||
|
||||
if (roomInstance.isFull)
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.InsufficientSpace);
|
||||
}
|
||||
|
||||
if (roomInstance.isPrivate || room.Accessibility == Accessibility.Private)
|
||||
{
|
||||
var hasGameInvite = await HasActiveGameInviteAsync(userId, roomInstance);
|
||||
var canManageRoom = await MatchmakingUtils.IsAtLeastRole(userId, roomInstance.roomId, RoleType.CoOwner);
|
||||
if (!hasGameInvite && !canManageRoom && room.CreatorAccountId != userId)
|
||||
{
|
||||
return MatchResponse.Error(roomInstance.isPrivate
|
||||
? JoinStatus.RoomInstanceIsPrivate
|
||||
: JoinStatus.RoomIsPrivate);
|
||||
}
|
||||
}
|
||||
|
||||
RealPresence? presence = await db.Presences
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.playerId == userId
|
||||
);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.Banned);
|
||||
}
|
||||
|
||||
presence.roomInstance = id;
|
||||
presence.isOnline = true;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
|
||||
await MatchmakingUtils.RecordRoomVisit(db, userId, roomInstance.roomId);
|
||||
await RoomJoinGiftUtils.TryDeliverAsync(db, userId, roomInstance.roomId);
|
||||
|
||||
MatchmakingUtils.MarkInRoom(userId);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (AdditionalPlayerIds != null &&
|
||||
AdditionalPlayerIds.Length > 0)
|
||||
{
|
||||
await InvitePlayers(
|
||||
AdditionalPlayerIds,
|
||||
roomInstance.roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
await Notify.UpdateSubscriptions(userId);
|
||||
return new MatchResponse { roomInstance = roomInstance, errorCode = JoinStatus.Success };
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/instance/{instanceId}"), Authorize]
|
||||
public async Task<MatchResponse?> MatchmakeInstance(
|
||||
long instanceId,
|
||||
[FromForm] GoToRequest? goTo)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
if (!nullableUserId.HasValue) return null;
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
RoomInstance? roomInstance = await db.RoomInstances
|
||||
.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId);
|
||||
if (roomInstance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
|
||||
RoomData? room = await db.Rooms
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId);
|
||||
if (room == null) return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
|
||||
bool isOwnDorm = room.IsDorm && room.CreatorAccountId == userId;
|
||||
if (!isOwnDorm)
|
||||
{
|
||||
if (await db.IsBanned(userId)) return MatchResponse.Error(JoinStatus.Banned);
|
||||
|
||||
bool managesRoom = await MatchmakingUtils.IsAtLeastRole(
|
||||
userId,
|
||||
room.RoomId,
|
||||
RoleType.CoOwner
|
||||
);
|
||||
|
||||
if (!managesRoom) return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
}
|
||||
|
||||
if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId))
|
||||
{
|
||||
return MatchResponse.Error(JoinStatus.BannedFromRoom);
|
||||
}
|
||||
|
||||
MatchResponse match = await MatchmakingUtils.JoinRoom(userId, instanceId);
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("goto/club/{clubId}"), Authorize]
|
||||
[HttpPost("matchmake/club/{clubId}"), Authorize]
|
||||
public async Task<MatchResponse?> GoToClub(long clubId, [FromForm] GoToRequest? goTo)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
if (!nullableUserId.HasValue) return null;
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
var club = await Clubs.ClubStore.GetAsync(db, clubId);
|
||||
if (club == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.RoomIsNotActive,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
var instance = await db.RoomInstances
|
||||
.Where(x => x.clubId == clubId)
|
||||
.OrderBy(x => x.roomInstanceId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
var clubhouseRoom = await db.Rooms
|
||||
.Where(x => x.CreatorAccountId == club.ownerPlayerId)
|
||||
.OrderByDescending(x => x.PublishedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (clubhouseRoom == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.RoomIsNotActive,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
instance = await MatchmakingUtils.CreateRoomInstance(clubhouseRoom.RoomId, false, null, clubId, null);
|
||||
}
|
||||
|
||||
var match = await MatchmakingUtils.JoinRoom(userId, instance!.roomInstanceId);
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("goto/event/{eventId}"), Authorize]
|
||||
[HttpPost("matchmake/event/{eventId}"), Authorize]
|
||||
public async Task<MatchResponse?> GoToEvent(
|
||||
long eventId,
|
||||
[FromForm] GoToRequest? goTo)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
|
||||
|
||||
|
||||
MatchResponse match =
|
||||
await MatchmakingUtils.MatchmakeEvent(
|
||||
userId,
|
||||
eventId,
|
||||
goTo?.JoinMode ?? JoinMode.PublicMatchmaking
|
||||
);
|
||||
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo?.AdditionalPlayerIds != null &&
|
||||
match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(
|
||||
goTo.AdditionalPlayerIds,
|
||||
match.roomInstance.roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/room/{roomId}"), Authorize]
|
||||
public async Task<MatchResponse?> MatchmakeRoomById(
|
||||
long roomId,
|
||||
[FromForm] GoToRequest goTo,
|
||||
[FromQuery] long? subRoomId = null)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
if (!nullableUserId.HasValue) return null;
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive);
|
||||
|
||||
MatchResponse match = await MatchmakingUtils.Matchmake(
|
||||
goTo.JoinMode,
|
||||
room.Name,
|
||||
userId,
|
||||
subRoomId);
|
||||
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo.AdditionalPlayerIds != null &&
|
||||
match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(
|
||||
goTo.AdditionalPlayerIds,
|
||||
match.roomInstance.roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/room/{roomId}/{subRoomId}"), Authorize]
|
||||
public async Task<MatchResponse?> MatchmakeRoomWithSubRoom(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
[FromForm] GoToRequest goTo)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
if (!nullableUserId.HasValue) return null;
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive);
|
||||
|
||||
MatchResponse match = await MatchmakingUtils.Matchmake(
|
||||
goTo.JoinMode,
|
||||
room.Name,
|
||||
userId,
|
||||
subRoomId);
|
||||
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo.AdditionalPlayerIds != null &&
|
||||
match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(
|
||||
goTo.AdditionalPlayerIds,
|
||||
match.roomInstance.roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("goto/room/{roomName}/{subRoomName}"),
|
||||
Authorize]
|
||||
public async Task<MatchResponse?> GoToSubRoom(
|
||||
string roomName,
|
||||
string subRoomName,
|
||||
[FromForm] GoToRequest goTo)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
roomName = Uri.UnescapeDataString(roomName)
|
||||
.Replace("+", " ");
|
||||
|
||||
subRoomName = Uri.UnescapeDataString(subRoomName)
|
||||
.Replace("+", " ");
|
||||
|
||||
RoomData? room = await db.Rooms.FirstOrDefaultAsync(
|
||||
x => x.Name == roomName
|
||||
);
|
||||
|
||||
if (room == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.RoomIsNotActive,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
SubRoom? subRoom = await db.SubRooms
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.RoomId == room.RoomId &&
|
||||
x.Name == subRoomName
|
||||
);
|
||||
|
||||
if (subRoom == null)
|
||||
{
|
||||
return new MatchResponse
|
||||
{
|
||||
errorCode = JoinStatus.RoomIsNotActive,
|
||||
roomInstance = null
|
||||
};
|
||||
}
|
||||
|
||||
MatchResponse match =
|
||||
await MatchmakingUtils.Matchmake(
|
||||
goTo.JoinMode,
|
||||
roomName,
|
||||
userId,
|
||||
subRoom.SubRoomId
|
||||
);
|
||||
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo.AdditionalPlayerIds != null &&
|
||||
match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(
|
||||
goTo.AdditionalPlayerIds,
|
||||
match.roomInstance.roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("goto/{name}"), Authorize]
|
||||
public async Task<MatchResponse?> GoTo(
|
||||
string name,
|
||||
[FromForm] GoToRequest goTo)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
name = Uri.UnescapeDataString(name)
|
||||
.Replace("+", " ");
|
||||
|
||||
MatchResponse match =
|
||||
await MatchmakingUtils.Matchmake(
|
||||
goTo.JoinMode,
|
||||
name,
|
||||
userId
|
||||
);
|
||||
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo.AdditionalPlayerIds != null &&
|
||||
match.roomInstance != null)
|
||||
{
|
||||
await InvitePlayers(
|
||||
goTo.AdditionalPlayerIds,
|
||||
match.roomInstance.roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("goto/player/{targetId}"), Authorize]
|
||||
public async Task<MatchResponse?> GoToPlayer(long targetId, [FromForm] GoToRequest? goTo)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
return userId == null ? null : await FollowPlayerIntoRoom(userId.Value, targetId, goTo);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/player/{targetId:long}"), Authorize]
|
||||
public async Task<MatchResponse?> MatchmakePlayer(long targetId, [FromForm] GoToRequest? goTo)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
return userId == null ? null : await FollowPlayerIntoRoom(userId.Value, targetId, goTo);
|
||||
}
|
||||
|
||||
private async Task<MatchResponse> FollowPlayerIntoRoom(long userId, long targetId, GoToRequest? goTo)
|
||||
{
|
||||
if (targetId <= 0 || targetId == userId ||
|
||||
!await db.Relationships.AsNoTracking().AnyAsync(relationship =>
|
||||
((relationship.AccountId == userId && relationship.PlayerId == targetId) ||
|
||||
(relationship.AccountId == targetId && relationship.PlayerId == userId)) &&
|
||||
relationship.RelationshipType == RadicadeServer.RecNet.Relationships.RelationshipType.Friend))
|
||||
return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
|
||||
var targetPresence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(presence => presence.playerId == targetId);
|
||||
if (targetPresence?.roomInstance == null)
|
||||
return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
|
||||
var instance = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == targetPresence.roomInstance.Value);
|
||||
if (instance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == instance.roomId);
|
||||
if (room == null) return MatchResponse.Error(JoinStatus.NoSuchRoom);
|
||||
|
||||
if (await MatchmakingUtils.IsBannedFromRoom(db, userId, room.RoomId))
|
||||
return MatchResponse.Error(JoinStatus.BannedFromRoom);
|
||||
|
||||
var presence = await db.Presences.FirstOrDefaultAsync(row => row.playerId == userId);
|
||||
if (presence == null)
|
||||
{
|
||||
presence = new RealPresence { playerId = userId };
|
||||
db.Presences.Add(presence);
|
||||
}
|
||||
presence.roomInstance = instance.roomInstanceId;
|
||||
presence.isOnline = true;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var match = new MatchResponse
|
||||
{
|
||||
roomInstance = MatchmakingUtils.NormalizeRoomInstance(instance),
|
||||
errorCode = JoinStatus.Success
|
||||
};
|
||||
await UpdatePresence(userId, match);
|
||||
if (goTo?.AdditionalPlayerIds is { Length: > 0 } additionalPlayerIds)
|
||||
await InvitePlayers(additionalPlayerIds, instance.roomInstanceId);
|
||||
return match;
|
||||
}
|
||||
|
||||
[HttpPost("invite"), Authorize]
|
||||
[HttpPost("invite/{playerId:long}"), Authorize]
|
||||
public async Task<Success?> Invite()
|
||||
{
|
||||
long? callerId = User.GetUserId();
|
||||
var (targetId, sentInstanceId, seen) = await ReadInviteFieldsAsync();
|
||||
|
||||
long instanceId = sentInstanceId;
|
||||
if (instanceId <= 0 && callerId.HasValue)
|
||||
{
|
||||
var presence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.playerId == callerId.Value);
|
||||
instanceId = presence?.roomInstance ?? 0;
|
||||
}
|
||||
|
||||
if (targetId <= 0)
|
||||
return InviteRefused("No invited player was named.");
|
||||
if (instanceId <= 0)
|
||||
return InviteRefused("The inviter is not in a room, so there is nothing to invite anyone to.");
|
||||
|
||||
var result = await InvitePlayer(targetId, instanceId);
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpPost("~/api/offlineinvite/v1/send"), Authorize]
|
||||
public async Task<Success> SendOfflineInvite()
|
||||
{
|
||||
long? callerId = User.GetUserId();
|
||||
var (targetId, sentInstanceId, seen) = await ReadInviteFieldsAsync();
|
||||
|
||||
long instanceId = sentInstanceId;
|
||||
if (instanceId <= 0 && callerId.HasValue)
|
||||
{
|
||||
var presence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.playerId == callerId.Value);
|
||||
instanceId = presence?.roomInstance ?? 0;
|
||||
}
|
||||
|
||||
if (targetId <= 0)
|
||||
return InviteRefused("No invited player was named.");
|
||||
if (instanceId <= 0)
|
||||
return InviteRefused("The inviter is not in a room, so there is nothing to invite anyone to.");
|
||||
|
||||
var result = await InvitePlayer(targetId, instanceId);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Success InviteRefused(string why)
|
||||
{
|
||||
return Success.Error(why);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/invite/{inviteId:long}"), Authorize]
|
||||
public async Task<MatchResponse?> AcceptInviteByMessageId(
|
||||
long inviteId,
|
||||
[FromForm] GoToRequest? goTo)
|
||||
{
|
||||
long? callerId = User.GetUserId();
|
||||
if (!callerId.HasValue) return null;
|
||||
long userId = callerId.Value;
|
||||
|
||||
Message? invite = await db.Messages.AsNoTracking()
|
||||
.Where(message =>
|
||||
message.Id == inviteId &&
|
||||
message.ToPlayerId == userId &&
|
||||
(message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2))
|
||||
.OrderByDescending(message => message.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (invite == null)
|
||||
return RefuseInviteAccept(
|
||||
$"invite {inviteId} is not addressed to account {userId}",
|
||||
JoinStatus.RoomInviteExpired);
|
||||
if (invite.FromPlayerId == userId)
|
||||
return RefuseInviteAccept(
|
||||
$"invite {inviteId} was sent by the caller",
|
||||
JoinStatus.ChatMessageNotAnInvite);
|
||||
var invitedInstanceId = RoomInviteDetails.GetInstanceId(invite);
|
||||
if (invitedInstanceId == null)
|
||||
return RefuseInviteAccept(
|
||||
$"invite {inviteId} names no room instance",
|
||||
JoinStatus.RoomInviteExpired);
|
||||
|
||||
RealPresence? inviterPresence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(presence => presence.playerId == invite.FromPlayerId);
|
||||
if (inviterPresence == null || !inviterPresence.isOnline)
|
||||
return RefuseInviteAccept(
|
||||
$"account {invite.FromPlayerId}, who sent invite {inviteId}, is not online",
|
||||
JoinStatus.PlayerNotOnline);
|
||||
if (inviterPresence.roomInstance != invitedInstanceId)
|
||||
return RefuseInviteAccept(
|
||||
$"invite {inviteId} names instance {invitedInstanceId} but its sender " +
|
||||
$"{invite.FromPlayerId} is now in {inviterPresence.roomInstance?.ToString() ?? "no instance"}",
|
||||
JoinStatus.RoomInviteExpired);
|
||||
|
||||
RoomInstance? instance = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == invitedInstanceId);
|
||||
if (instance == null)
|
||||
return RefuseInviteAccept(
|
||||
$"instance {invitedInstanceId} from invite {inviteId} is not live",
|
||||
JoinStatus.RoomIsNotActive);
|
||||
|
||||
return await AcceptInviteIntoInstanceAsync(
|
||||
userId, invite.FromPlayerId, instance, goTo, $"invite {inviteId}");
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/chatinvite/{threadId:long}/{messageId:long}"), Authorize]
|
||||
public async Task<MatchResponse?> MatchmakeChatInvite(
|
||||
long threadId,
|
||||
long messageId,
|
||||
[FromForm] GoToRequest? goTo)
|
||||
{
|
||||
long? callerId = User.GetUserId();
|
||||
if (!callerId.HasValue) return null;
|
||||
long userId = callerId.Value;
|
||||
|
||||
ChatThread? thread = await db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId);
|
||||
var members = thread?.GetPlayerIds();
|
||||
if (thread == null || members == null || !members.Contains(userId))
|
||||
return RefuseInviteAccept(
|
||||
$"chat invite {messageId}: thread {threadId} is not a conversation account {userId} is part of",
|
||||
JoinStatus.ChatPartyInviteNotFound);
|
||||
|
||||
ChatMessage? message = thread.GetMessages()?.FirstOrDefault(x => x.chatMessageId == messageId);
|
||||
long inviterId;
|
||||
string via;
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
if (message.senderPlayerId == userId)
|
||||
return RefuseInviteAccept(
|
||||
$"chat message {messageId} in thread {threadId} was written by the caller, so it is not an invite",
|
||||
JoinStatus.ChatMessageNotAnInvite);
|
||||
if (DateTime.UtcNow - message.timeSent >
|
||||
TimeSpan.FromMinutes(PlayerSettingsAPI.InviteLinkLifetimeInMinutes))
|
||||
return RefuseInviteAccept(
|
||||
$"chat invite {messageId} in thread {threadId} is older than " +
|
||||
$"{PlayerSettingsAPI.InviteLinkLifetimeInMinutes} minutes",
|
||||
JoinStatus.RoomInviteExpired);
|
||||
|
||||
inviterId = message.senderPlayerId;
|
||||
via = $"chat invite {messageId} in thread {threadId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var others = members.Where(member => member != userId).Distinct().ToList();
|
||||
if (others.Count != 1)
|
||||
return RefuseInviteAccept(
|
||||
$"chat invite {messageId} in thread {threadId} is not stored and the thread " +
|
||||
$"has {others.Count} other member(s) to resolve an inviter from",
|
||||
JoinStatus.ChatPartyInviteNotFound);
|
||||
|
||||
inviterId = others[0];
|
||||
via = $"thread {threadId} member {inviterId} (message {messageId} is not stored)";
|
||||
}
|
||||
|
||||
RealPresence? inviterPresence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(presence => presence.playerId == inviterId);
|
||||
if (inviterPresence == null || !inviterPresence.isOnline ||
|
||||
inviterPresence.roomInstance is null or <= 0)
|
||||
return RefuseInviteAccept(
|
||||
$"{via}: inviter {inviterId} is not in a room instance",
|
||||
JoinStatus.PlayerNotOnline);
|
||||
|
||||
RoomInstance? instance = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == inviterPresence.roomInstance.Value);
|
||||
if (instance == null)
|
||||
return RefuseInviteAccept(
|
||||
$"{via}: inviter {inviterId} is in instance {inviterPresence.roomInstance.Value}, which is not live",
|
||||
JoinStatus.RoomIsNotActive);
|
||||
|
||||
return await AcceptInviteIntoInstanceAsync(userId, inviterId, instance, goTo, via);
|
||||
}
|
||||
|
||||
private async Task<MatchResponse> AcceptInviteIntoInstanceAsync(
|
||||
long userId,
|
||||
long inviterId,
|
||||
RoomInstance instance,
|
||||
GoToRequest? goTo,
|
||||
string via)
|
||||
{
|
||||
if (await db.IsBanned(userId))
|
||||
return RefuseInviteAccept($"{via}: account {userId} is banned", JoinStatus.Banned);
|
||||
if (await MatchmakingUtils.IsBannedFromRoom(db, userId, instance.roomId))
|
||||
return RefuseInviteAccept(
|
||||
$"{via}: account {userId} is banned from room {instance.roomId}",
|
||||
JoinStatus.BannedFromRoom);
|
||||
|
||||
RoomData? room = await db.Rooms.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == instance.roomId);
|
||||
if (room == null)
|
||||
return RefuseInviteAccept($"{via}: room {instance.roomId} is gone", JoinStatus.RoomIsNotActive);
|
||||
|
||||
var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync(
|
||||
db, userId, room.IsDorm && room.CreatorAccountId == userId,
|
||||
$"instance {instance.roomInstanceId} (room {instance.roomId})");
|
||||
if (warningBounce != null) return warningBounce;
|
||||
|
||||
RealPresence? presence = await db.Presences.FirstOrDefaultAsync(row => row.playerId == userId);
|
||||
if (presence == null)
|
||||
{
|
||||
presence = new RealPresence { playerId = userId };
|
||||
db.Presences.Add(presence);
|
||||
}
|
||||
|
||||
presence.roomInstance = instance.roomInstanceId;
|
||||
presence.isOnline = true;
|
||||
presence.errorCode = JoinStatus.Success;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var match = new MatchResponse
|
||||
{
|
||||
roomInstance = MatchmakingUtils.NormalizeRoomInstance(instance),
|
||||
errorCode = JoinStatus.Success
|
||||
};
|
||||
await UpdatePresence(userId, match);
|
||||
|
||||
if (goTo?.AdditionalPlayerIds is { Length: > 0 } additionalPlayerIds)
|
||||
await InvitePlayers(additionalPlayerIds, instance.roomInstanceId);
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
private MatchResponse RefuseInviteAccept(string why, JoinStatus status)
|
||||
{
|
||||
return MatchResponse.Error(status);
|
||||
}
|
||||
|
||||
private static readonly string[] InviteTargetKeys =
|
||||
{
|
||||
"playerId", "PlayerId", "playerIds", "PlayerIds", "id", "Id",
|
||||
"invitedPlayerId", "InvitedPlayerId", "invitedPlayerIds", "InvitedPlayerIds",
|
||||
"invitedPlayerIds[]", "InvitedPlayerIds[]", "targetPlayerId", "TargetPlayerId",
|
||||
"accountId", "AccountId",
|
||||
};
|
||||
|
||||
private static readonly string[] InviteInstanceKeys =
|
||||
{
|
||||
"roomInstanceId", "RoomInstanceId", "instanceId", "InstanceId", "roomInstance", "RoomInstance",
|
||||
};
|
||||
|
||||
private async Task<(long PlayerId, long RoomInstanceId, string Seen)> ReadInviteFieldsAsync()
|
||||
{
|
||||
long playerId = 0;
|
||||
long roomInstanceId = 0;
|
||||
var seen = new List<string>();
|
||||
|
||||
void Collect(string key, string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return;
|
||||
seen.Add($"{key}={value}");
|
||||
|
||||
if (InviteTargetKeys.Contains(key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var part in value.Split(new[] { ',', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
if (playerId <= 0 && long.TryParse(part, out var parsed) && parsed > 0) playerId = parsed;
|
||||
}
|
||||
else if (InviteInstanceKeys.Contains(key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (roomInstanceId <= 0 && long.TryParse(value.Trim(), out var parsed) && parsed > 0)
|
||||
roomInstanceId = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pair in Request.RouteValues)
|
||||
if (pair.Value != null) Collect(pair.Key, pair.Value.ToString());
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var field in form)
|
||||
foreach (var value in field.Value)
|
||||
Collect(field.Key, value);
|
||||
}
|
||||
else if (Request.ContentLength is > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
if (await JsonNode.ParseAsync(Request.Body) is JsonObject body)
|
||||
foreach (var pair in body)
|
||||
{
|
||||
if (pair.Value is JsonArray array)
|
||||
foreach (var item in array) Collect(pair.Key, item?.ToString());
|
||||
else
|
||||
Collect(pair.Key, pair.Value?.ToString());
|
||||
}
|
||||
|
||||
Request.Body.Position = 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { Request.Body.Position = 0; } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in InviteTargetKeys.Concat(InviteInstanceKeys))
|
||||
foreach (var value in Request.Query[key])
|
||||
Collect(key, value);
|
||||
|
||||
return (playerId, roomInstanceId, seen.Count == 0 ? "(none)" : string.Join("; ", seen));
|
||||
}
|
||||
|
||||
public async Task<Success> UpdatePresence(
|
||||
long userId,
|
||||
MatchResponse match)
|
||||
{
|
||||
RealPresence? realPresence = await db.Presences
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.playerId == userId
|
||||
);
|
||||
|
||||
if (realPresence == null)
|
||||
{
|
||||
return new Success(
|
||||
false,
|
||||
"Presence does not exist.",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (match.errorCode == JoinStatus.Success &&
|
||||
match.roomInstance != null)
|
||||
{
|
||||
RoomInstance roomInstance =
|
||||
match.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;
|
||||
|
||||
realPresence.roomInstance =
|
||||
roomInstance.roomInstanceId;
|
||||
|
||||
realPresence.isOnline = true;
|
||||
realPresence.errorCode =
|
||||
JoinStatus.Success;
|
||||
|
||||
MatchmakingUtils.MarkInRoom(userId);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
realPresence.errorCode = match.errorCode;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var presence = await Presence.CreateAsync(db,
|
||||
realPresence,
|
||||
match.roomInstance
|
||||
);
|
||||
|
||||
var subscriberUpdate = Notify.UpdateSubscriptions(userId);
|
||||
try
|
||||
{
|
||||
await Notify.SendNotify(
|
||||
userId,
|
||||
new Notification
|
||||
{
|
||||
Id = "PresenceUpdate",
|
||||
Msg = presence
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await subscriberUpdate;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
public async Task<Success> InvitePlayers(
|
||||
long[] players,
|
||||
long roomInstanceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (long playerId in players)
|
||||
{
|
||||
await InvitePlayer(
|
||||
playerId,
|
||||
roomInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
return new Success(
|
||||
false,
|
||||
"Could not invite additional player ids.",
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Success> InvitePlayer(
|
||||
long playerId,
|
||||
long roomInstanceId)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return Success.Error("Missing user ID.");
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
if (playerId <= 0 || playerId == userId) return Success.Error("Invalid player ID.");
|
||||
|
||||
RoomInstance? roomInstance = await db.RoomInstances
|
||||
.FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId);
|
||||
if (roomInstance == null) return Success.Error("Room instance not found.");
|
||||
|
||||
Profile? target = await db.Profiles
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.accountId == playerId);
|
||||
if (target == null) return Success.Error("Player not found.");
|
||||
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId);
|
||||
if (room == null) return Success.Error("Room not found.");
|
||||
if (roomInstance.isFull) return Success.Error("Room instance is full.");
|
||||
if (await db.IsBanned(userId)) return Success.Error("Inviter is banned.");
|
||||
if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId))
|
||||
return Success.Error("Inviter is banned from this room.");
|
||||
if (await MatchmakingUtils.IsBannedFromRoom(db, playerId, roomInstance.roomId))
|
||||
return Success.Error("Player is banned from this room.");
|
||||
|
||||
var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync(
|
||||
db, userId, room.IsDorm && room.CreatorAccountId == userId, $"instance {roomInstanceId}");
|
||||
if (warningBounce != null) return Success.Error("Inviter is blocked from the room.");
|
||||
|
||||
var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == userId);
|
||||
if (presence?.roomInstance != roomInstanceId || !presence.isOnline)
|
||||
return Success.Error("Inviter must be online in the room instance.");
|
||||
|
||||
var playerCount = await db.Presences.CountAsync(row =>
|
||||
row.roomInstance == roomInstanceId && row.isOnline);
|
||||
if (playerCount >= roomInstance.maxCapacity)
|
||||
return Success.Error("Room instance is full.");
|
||||
|
||||
|
||||
var details = new RoomInviteDetails
|
||||
{
|
||||
Name = room.Name,
|
||||
RoomInstanceId = roomInstanceId,
|
||||
InviteMode = 0
|
||||
};
|
||||
await Notify.SendMessage(playerId, new Message
|
||||
{
|
||||
Type = MessageType.GameInviteV2,
|
||||
RoomId = roomInstance.roomId,
|
||||
FromPlayerId = userId
|
||||
}, prepareWithId: message =>
|
||||
{
|
||||
details.InviteId = message.Id;
|
||||
message.Data = System.Text.Json.JsonSerializer.Serialize(details);
|
||||
});
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
private async Task<bool> HasActiveGameInviteAsync(long playerId, RoomInstance roomInstance)
|
||||
{
|
||||
var invites = await db.Messages.AsNoTracking()
|
||||
.Where(message =>
|
||||
message.ToPlayerId == playerId &&
|
||||
(message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2) &&
|
||||
message.RoomId == roomInstance.roomId)
|
||||
.OrderByDescending(message => message.Id)
|
||||
.ToListAsync();
|
||||
var invite = invites.FirstOrDefault(message =>
|
||||
RoomInviteDetails.GetInstanceId(message) == roomInstance.roomInstanceId);
|
||||
if (invite == null || await db.IsBanned(invite.FromPlayerId)) return false;
|
||||
|
||||
return await db.Presences.AsNoTracking().AnyAsync(presence =>
|
||||
presence.playerId == invite.FromPlayerId &&
|
||||
presence.roomInstance == roomInstance.roomInstanceId &&
|
||||
presence.isOnline) &&
|
||||
!await MatchmakingUtils.IsBannedFromRoom(db, invite.FromPlayerId, roomInstance.roomId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.Auth;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Enums.Players;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
namespace RadicadeServer.API.Matchmaking
|
||||
{
|
||||
[Route("matchmaking/player")]
|
||||
public class MatchmakingPlayerAPI : Controller
|
||||
{
|
||||
private readonly RecDBContext db;
|
||||
|
||||
public MatchmakingPlayerAPI(RecDBContext context)
|
||||
{
|
||||
db = context;
|
||||
}
|
||||
|
||||
[HttpPut("photonregionpings")]
|
||||
public IActionResult SendPhotonRegionPings()
|
||||
{
|
||||
return Ok(new { });
|
||||
}
|
||||
|
||||
[HttpPut("gameserverregionpings")]
|
||||
public IActionResult SendGameServerRegionPings()
|
||||
{
|
||||
return Ok(new { });
|
||||
}
|
||||
|
||||
[HttpGet("qos")]
|
||||
public IActionResult GetQos()
|
||||
{
|
||||
return Ok(new[]
|
||||
{
|
||||
new { id = "us-west1", address = "34.169.254.144:50000" },
|
||||
new { id = "europe-west1", address = "35.205.141.119:50000" },
|
||||
new { id = "asia-northeast1", address = "35.200.67.228:50000" },
|
||||
new { id = "us-east1", address = "34.73.244.122:50000" },
|
||||
new { id = "us-central1", address = "34.69.179.51:50000" },
|
||||
new { id = "northamerica-northeast1", address = "34.152.4.100:50000" }
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("connection-info"), Authorize]
|
||||
public async Task<IActionResult> GetConnectionInfo([FromQuery] long? roomInstanceId = null)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
string photonRoomId = string.Empty;
|
||||
var presence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.playerId == userId);
|
||||
if (presence?.roomInstance.HasValue == true)
|
||||
{
|
||||
var instance = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value);
|
||||
photonRoomId = instance?.photonRoomId ?? string.Empty;
|
||||
}
|
||||
if (string.IsNullOrEmpty(photonRoomId) && roomInstanceId.HasValue)
|
||||
{
|
||||
var requested = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId.Value);
|
||||
photonRoomId = requested?.photonRoomId ?? string.Empty;
|
||||
}
|
||||
|
||||
var platformId = User.GetPlatId() ?? string.Empty;
|
||||
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey);
|
||||
var now = DateTime.UtcNow;
|
||||
var photonToken = tokenHandler.WriteToken(tokenHandler.CreateToken(
|
||||
new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new System.Security.Claims.ClaimsIdentity(new[]
|
||||
{
|
||||
new System.Security.Claims.Claim("sub", userId.ToString()),
|
||||
new System.Security.Claims.Claim("rn.platid", platformId),
|
||||
new System.Security.Claims.Claim("rn.plat", User.FindFirst("rn.plat")?.Value ?? "0"),
|
||||
new System.Security.Claims.Claim("rn.deviceclass", User.FindFirst("rn.deviceclass")?.Value ?? "0"),
|
||||
new System.Security.Claims.Claim("rn.env", "prod")
|
||||
}),
|
||||
Issuer = DBConfig.TokenIssuer,
|
||||
Expires = now.AddHours(1),
|
||||
SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
|
||||
new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(key),
|
||||
Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature)
|
||||
}));
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
value = new
|
||||
{
|
||||
photonAuthToken = photonToken,
|
||||
photonRealtimeAppId = DBConfig.PhotonRealtimeAppId,
|
||||
photonVoiceAppId = DBConfig.PhotonVoiceAppId,
|
||||
photonChatAppId = DBConfig.PhotonChatAppId,
|
||||
photonRegion = DBConfig.PhotonRegion,
|
||||
photonRoomId,
|
||||
voiceConnectionInfo = string.Empty,
|
||||
voiceServerId = string.Empty,
|
||||
experiments = new
|
||||
{
|
||||
networkTransformSyncInterval = 10.0,
|
||||
shouldUseUnreliableOnChange = false,
|
||||
shouldAvoidDiscontinuityRPCs = true,
|
||||
shouldAvoidRedundantDiscontinuity = false,
|
||||
r2RuntimeStaticBaking = true,
|
||||
r2AutoEmbodiment = true,
|
||||
r2RuntimeStaticBakingMinShapeThreshold = 1,
|
||||
r2UseCheapReplicas = true,
|
||||
shouldUseGameServerNetworking = false
|
||||
}
|
||||
},
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("avoidjuniors"), Authorize]
|
||||
public IActionResult GetAvoidJuniors()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
[HttpPut("avoidjuniors"), Authorize]
|
||||
public IActionResult SetAvoidJuniors()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
[HttpPut("statusvisibility"), Authorize]
|
||||
public async Task<IActionResult> SetStatusVisibility(
|
||||
StatusVisibility statusVisibility)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return Unauthorized("Missing user ID.");
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
var presence = await db.Presences
|
||||
.FirstOrDefaultAsync(x => x.playerId == userId);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
return NotFound("Player presence does not exist.");
|
||||
}
|
||||
|
||||
presence.statusVisibility = statusVisibility;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await Notify.UpdateSubscriptions(userId);
|
||||
|
||||
return Ok(new { });
|
||||
}
|
||||
|
||||
[HttpPost("heartbeat"), Authorize]
|
||||
public async Task<IActionResult> Heartbeat()
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return Unauthorized("Missing user ID.");
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
try
|
||||
{
|
||||
return await HeartbeatCore(userId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
return Ok(new PlayerHeartbeat
|
||||
{
|
||||
playerId = userId,
|
||||
isOnline = true,
|
||||
deviceClass = DeviceClass.Screen,
|
||||
statusVisibility = StatusVisibility.Public,
|
||||
roomInstance = null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HeartbeatCore(long userId)
|
||||
{
|
||||
var presence = await db.Presences
|
||||
.FirstOrDefaultAsync(x => x.playerId == userId);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
|
||||
presence = new RealPresence { playerId = userId };
|
||||
db.Presences.Add(presence);
|
||||
|
||||
await MatchmakingUtils.EnsureDormPresenceAsync(db, userId, presence);
|
||||
}
|
||||
|
||||
RoomInstance? roomInstance = null;
|
||||
|
||||
if (presence.roomInstance.HasValue)
|
||||
{
|
||||
long roomInstanceId = presence.roomInstance.Value;
|
||||
|
||||
roomInstance = await db.RoomInstances
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.roomInstanceId == roomInstanceId
|
||||
);
|
||||
|
||||
if (roomInstance == null)
|
||||
{
|
||||
presence.roomInstance = null;
|
||||
MatchmakingUtils.ClearInRoom(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (roomInstance != null)
|
||||
{
|
||||
roomInstance.eventId ??= 0;
|
||||
roomInstance.clubId ??= 0;
|
||||
roomInstance.roomCode ??= string.Empty;
|
||||
roomInstance.location ??= string.Empty;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (roomInstance != null)
|
||||
{
|
||||
var currentRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId);
|
||||
bool isOwnDorm = currentRoom != null && currentRoom.IsDorm && currentRoom.CreatorAccountId == userId;
|
||||
if (!isOwnDorm)
|
||||
{
|
||||
var ban = await db.ResolveBanAsync(userId, Request.GetClientIp());
|
||||
|
||||
if (ban != null && !await db.IsDeveloper(userId))
|
||||
{
|
||||
await PlayerUtils.EjectToDormAsync(db, userId);
|
||||
roomInstance = presence.roomInstance.HasValue
|
||||
? await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value)
|
||||
: null;
|
||||
|
||||
await Notify.SendNotify(userId, new Notification
|
||||
{
|
||||
Id = NotificationType.ModerationKick,
|
||||
Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, userId)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
presence.isOnline = true;
|
||||
presence.deviceClass = DeviceClass.Screen;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (roomInstance != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var xpRoom = await db.Rooms.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId);
|
||||
bool publicRoom = xpRoom != null && !xpRoom.IsDorm &&
|
||||
xpRoom.Accessibility == RadicadeServer.RecNet.Enums.Accessibility.Public;
|
||||
await RoomProgressionService.AwardRoomXpAsync(db, userId, roomInstance.roomId, publicRoom);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var heartbeat = new PlayerHeartbeat
|
||||
{
|
||||
deviceClass = presence.deviceClass,
|
||||
isOnline = true,
|
||||
playerId = userId,
|
||||
roomInstance = roomInstance,
|
||||
statusVisibility = presence.statusVisibility
|
||||
};
|
||||
|
||||
if (roomInstance != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var presenceUpdate =
|
||||
await Presence.CreateAsync(db, presence, roomInstance);
|
||||
|
||||
await Notify.SendNotify(
|
||||
userId,
|
||||
new Notification
|
||||
{
|
||||
Id = "PresenceUpdate",
|
||||
Msg = presenceUpdate
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Notify.UpdateSubscriptions(userId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpPost("notifydisconnect"), Authorize]
|
||||
public async Task<IActionResult> NotifyDisconnect(
|
||||
[FromForm(Name = "playerId")] long playerId = 0,
|
||||
[FromForm(Name = "loginLock")] string? loginLock = null)
|
||||
{
|
||||
var requesterId = User.GetUserId();
|
||||
if (!requesterId.HasValue) return Unauthorized();
|
||||
|
||||
long targetId = playerId > 0 ? playerId : 0;
|
||||
|
||||
|
||||
|
||||
if (targetId == 0 && !string.IsNullOrWhiteSpace(loginLock))
|
||||
{
|
||||
var byLock = await db.Presences.FirstOrDefaultAsync(x => x.LoginLock == loginLock);
|
||||
if (byLock != null) targetId = byLock.playerId;
|
||||
}
|
||||
|
||||
if (targetId == 0) targetId = requesterId.Value;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var ban = await db.ResolveBanAsync(targetId, Request.GetClientIp());
|
||||
if (ban == null) return Ok();
|
||||
|
||||
|
||||
|
||||
await PlayerUtils.EjectToDormAsync(db, targetId);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (targetId != requesterId.Value)
|
||||
{
|
||||
await Notify.SendNotify(targetId, new Notification
|
||||
{
|
||||
Id = NotificationType.ModerationKick,
|
||||
Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, targetId)
|
||||
});
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("login"), Authorize]
|
||||
public async Task<IActionResult> Login(
|
||||
[FromForm(Name = "LoginLock")]
|
||||
string? loginLock = null,
|
||||
|
||||
[FromForm(Name = "identification")]
|
||||
string? identification = null,
|
||||
|
||||
[FromForm(Name = "JoinMode")]
|
||||
int joinMode = 0,
|
||||
|
||||
[FromForm(Name = "BypassMovementModeRestriction")]
|
||||
bool bypassMovementModeRestriction = false)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
|
||||
return Unauthorized(new
|
||||
{
|
||||
error = "invalid_token",
|
||||
error_description = "Missing user ID."
|
||||
});
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
string? platformId = User.GetPlatId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(platformId))
|
||||
{
|
||||
|
||||
return Unauthorized(new
|
||||
{
|
||||
error = "invalid_token",
|
||||
error_description = "Missing platform ID."
|
||||
});
|
||||
}
|
||||
|
||||
bool isWhitelisted;
|
||||
|
||||
try
|
||||
{
|
||||
isWhitelisted =
|
||||
await AuthShared.IsWhitelisted(platformId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
return StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
new
|
||||
{
|
||||
error = "server_error",
|
||||
error_description =
|
||||
"Whitelist check failed."
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (!isWhitelisted)
|
||||
{
|
||||
|
||||
return Unauthorized(new
|
||||
{
|
||||
error = "not_whitelisted",
|
||||
error_description =
|
||||
"This account is not whitelisted."
|
||||
});
|
||||
}
|
||||
|
||||
var presence = await db.Presences
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.playerId == userId
|
||||
);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
|
||||
presence = new RealPresence
|
||||
{
|
||||
playerId = userId
|
||||
};
|
||||
|
||||
db.Presences.Add(presence);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(loginLock))
|
||||
{
|
||||
presence.LoginLock = loginLock;
|
||||
}
|
||||
|
||||
presence.isOnline = true;
|
||||
presence.deviceClass = DeviceClass.Screen;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (!presence.roomInstance.HasValue)
|
||||
{
|
||||
await MatchmakingUtils.EnsureDormPresenceAsync(
|
||||
db,
|
||||
userId,
|
||||
presence);
|
||||
}
|
||||
else
|
||||
{
|
||||
presence.isOnline = true;
|
||||
}
|
||||
|
||||
if (presence.roomInstance.HasValue)
|
||||
{
|
||||
MatchmakingUtils.MarkInRoom(userId);
|
||||
}
|
||||
else
|
||||
{
|
||||
MatchmakingUtils.ClearInRoom(userId);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await Notify.UpdateSubscriptions(userId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
playerId = userId,
|
||||
accountId = userId,
|
||||
platformId,
|
||||
loginLock = presence.LoginLock ??
|
||||
string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("exclusivelogin"), Authorize]
|
||||
public Task<IActionResult> ExclusiveLogin(
|
||||
[FromForm(Name = "LoginLock")]
|
||||
string? loginLock = null,
|
||||
|
||||
[FromForm(Name = "identification")]
|
||||
string? identification = null,
|
||||
|
||||
[FromForm(Name = "JoinMode")]
|
||||
int joinMode = 0,
|
||||
|
||||
[FromForm(Name = "BypassMovementModeRestriction")]
|
||||
bool bypassMovementModeRestriction = false)
|
||||
{
|
||||
return Login(
|
||||
loginLock,
|
||||
identification,
|
||||
joinMode,
|
||||
bypassMovementModeRestriction);
|
||||
}
|
||||
|
||||
[HttpPost("logout"), Authorize]
|
||||
public async Task<IActionResult> Logout(
|
||||
[FromForm(Name = "LoginLock")]
|
||||
string? loginLock = null)
|
||||
{
|
||||
long? nullableUserId = User.GetUserId();
|
||||
|
||||
if (!nullableUserId.HasValue)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
long userId = nullableUserId.Value;
|
||||
|
||||
var presence = await db.Presences
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.playerId == userId
|
||||
);
|
||||
|
||||
if (presence == null)
|
||||
{
|
||||
MatchmakingUtils.ClearInRoom(userId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
presence.LoginLock = null;
|
||||
presence.isOnline = false;
|
||||
presence.roomInstance = null;
|
||||
|
||||
MatchmakingUtils.ClearInRoom(userId);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var subscriberUpdate = Notify.UpdateSubscriptions(userId);
|
||||
try
|
||||
{
|
||||
await Notify.SendNotify(userId, new Notification
|
||||
{
|
||||
Id = "PresenceUpdate",
|
||||
Msg = await Presence.CreateAsync(db, presence, null)
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
await subscriberUpdate;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
|
||||
namespace RadicadeServer.Utils;
|
||||
|
||||
public static class MatchmakingRuntimeState
|
||||
{
|
||||
public static async Task ResetAsync(RecDBContext db)
|
||||
{
|
||||
var presences = await db.Presences.Where(p => p.isOnline).ToListAsync();
|
||||
foreach (var presence in presences)
|
||||
{
|
||||
presence.isOnline = false;
|
||||
presence.roomInstance = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
var broadcastingEvents = await db.PlayerEvents
|
||||
.Where(ev => ev.BroadcastingRoomInstanceId != null).ToListAsync();
|
||||
foreach (var ev in broadcastingEvents)
|
||||
ev.BroadcastingRoomInstanceId = null;
|
||||
|
||||
db.RoomInstances.RemoveRange(await db.RoomInstances.ToListAsync());
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -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("-", "")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace RadicadeServer.RecNet.Matchmaking
|
||||
{
|
||||
public class PlayerQuickPlayDTO
|
||||
{
|
||||
public int? TargetPlayerId { get; private set; }
|
||||
public string RoomName { get; private set; }
|
||||
|
||||
public PlayerQuickPlayDTO(int? targetPlayerId, string roomName)
|
||||
{
|
||||
TargetPlayerId = targetPlayerId;
|
||||
RoomName = roomName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Matchmaking;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace RadicadeServer.API.Matchmaking
|
||||
{
|
||||
[Route("api/quickPlay")]
|
||||
public class QuickplayAPI : Controller
|
||||
{
|
||||
RecDBContext db;
|
||||
public QuickplayAPI(RecDBContext context)
|
||||
{
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
[HttpGet("v1/getandclear"), Authorize]
|
||||
public async Task<PlayerQuickPlayDTO?> GetAndClearQuickPlay()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var presence = await db.Presences.FirstOrDefaultAsync((x) => x.playerId == userId);
|
||||
var presenceRoomInstance = presence?.roomInstance;
|
||||
RoomInstance? roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presenceRoomInstance);
|
||||
|
||||
string roomTarget = "DormRoom";
|
||||
if (roomInstance != null)
|
||||
roomTarget = roomInstance.name;
|
||||
|
||||
return new PlayerQuickPlayDTO(null, roomTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
|
||||
namespace RadicadeServer.Matchmaking
|
||||
{
|
||||
[Route("matchmaking/roominstance/")]
|
||||
public class RoomInstanceAPI : Controller
|
||||
{
|
||||
RecDBContext db;
|
||||
public RoomInstanceAPI(RecDBContext context)
|
||||
{
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
[HttpPut("{id}/markprivate")]
|
||||
public async Task<IActionResult> MarkPrivate(long id, bool isPrivate)
|
||||
{
|
||||
var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == id);
|
||||
if (roomInstance == null) return Ok(new { success = false, error = "Room instance not found" });
|
||||
|
||||
roomInstance.isPrivate = isPrivate;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
[HttpPut("{id}/inprogress")]
|
||||
public async Task<IActionResult> MarkInProgress(long id, bool inProgress)
|
||||
{
|
||||
var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == id);
|
||||
if (roomInstance == null) return Ok(new { success = false, error = "Room instance not found" });
|
||||
|
||||
roomInstance.isInProgress = inProgress;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
[HttpGet("{id}/roomCode")]
|
||||
public IActionResult SetRoomCode([FromForm] string roomCode, [FromQuery] long id)
|
||||
{
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user