mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
1239 lines
45 KiB
C#
1239 lines
45 KiB
C#
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);
|
|
}
|
|
|
|
}
|
|
}
|