mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Refactor configuration endpoints and update client installation scripts
- Removed Photon configuration fields from dorknet-clientmod.json.template. - Added new backtrace and Azure Speech configuration endpoints in ConfigController. - Updated response format for IsCreationEnabled and IsRenderingEnabled endpoints in CustomAvatarItemsController. - Enhanced InfluencerPartnerProgramController to include additional player properties. - Implemented RoomKeepsakes endpoint in KeepsakesController to retrieve keepsakes by room ID. - Modified ReportsController to standardize response for Hile reporting. - Added PlayerDataForMe and RoomsRequiring endpoints in RoomsController for enhanced player data retrieval. - Introduced new dorm matchmaking methods in MatchController for player session management. - Updated MatchPlayerController to handle exclusive login functionality. - Revised install scripts to reflect changes in Photon AppId handling and patching process. - Added new patch-client.ps1 script for applying static byte patches to the Rec Room client.
This commit is contained in:
@@ -17,10 +17,12 @@ namespace DorkNet.Server.Controllers.Match;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class MatchController(
|
||||
IConfiguration config,
|
||||
GameSessionService sessionService,
|
||||
PlayerPresenceService presence,
|
||||
PrivateInstanceService privateInstances,
|
||||
JoinTimeoutService joinTimeouts,
|
||||
RoomService rooms,
|
||||
DorkNetDbContext db,
|
||||
ILogger<MatchController> logger) : ControllerBase
|
||||
{
|
||||
@@ -84,6 +86,138 @@ public class MatchController(
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("/matchmake/none")]
|
||||
public async Task<IActionResult> MatchmakeNone()
|
||||
{
|
||||
var pid = this.CurrentPlayerId() ?? 0;
|
||||
if (pid == 0) return Unauthorized();
|
||||
|
||||
var current = presence.GetRoom(pid) ?? await BuildDormRoomInstanceAsync(pid);
|
||||
presence.SetRoom(pid, current);
|
||||
presence.MarkActive(pid);
|
||||
logger.LogInformation("[matchmake-none] player={Player}", pid);
|
||||
return Ok(new MatchmakingResponseDto
|
||||
{
|
||||
ErrorCode = 0,
|
||||
RoomInstance = current,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/matchmake/dorm")]
|
||||
public async Task<IActionResult> MatchmakeDorm()
|
||||
{
|
||||
var pid = this.CurrentPlayerId() ?? 0;
|
||||
if (pid == 0) return Unauthorized();
|
||||
|
||||
var room = await BuildDormRoomInstanceAsync(pid);
|
||||
presence.SetRoom(pid, room);
|
||||
presence.MarkActive(pid);
|
||||
await RecordVisitAsync(pid, room.RoomId);
|
||||
logger.LogInformation(
|
||||
"[matchmake-dorm] player={Player} room={Room} instance={Instance} photon={Photon}",
|
||||
pid, room.RoomId, room.RoomInstanceId, room.PhotonRoomId);
|
||||
return Ok(new MatchmakingResponseDto
|
||||
{
|
||||
ErrorCode = 0,
|
||||
RoomInstance = room,
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<RoomInstanceDto> BuildDormRoomInstanceAsync(long playerId)
|
||||
{
|
||||
var personal = await rooms.EnsurePersonalDormAsync(playerId);
|
||||
var dataBlob = await rooms.ResolveDormDataBlobNameAsync(playerId, personal.Id);
|
||||
if (string.IsNullOrWhiteSpace(dataBlob))
|
||||
dataBlob = personal.CurrentDataBlobName;
|
||||
|
||||
var photonRegion = (config["Photon:CloudRegion"] ?? "us").ToLowerInvariant();
|
||||
var photonRoomId = $"^dormroom_p{playerId}";
|
||||
var dormInstanceName = await BuildDormInstanceNameAsync(playerId);
|
||||
|
||||
var dorm = await privateInstances.EnsureForDormAsync(
|
||||
ownerPlayerId: playerId,
|
||||
roomId: personal.Id,
|
||||
subRoomId: 0,
|
||||
baseName: dormInstanceName,
|
||||
location: personal.LocationReplicationId,
|
||||
dataBlob: dataBlob ?? string.Empty,
|
||||
photonRegion: photonRegion,
|
||||
maxCapacity: personal.MaxCapacity,
|
||||
photonRoomId: photonRoomId);
|
||||
|
||||
return new RoomInstanceDto
|
||||
{
|
||||
RoomInstanceId = dorm.InstanceId,
|
||||
RoomId = personal.Id,
|
||||
SubRoomId = 0,
|
||||
Location = personal.LocationReplicationId,
|
||||
PhotonRegionId = photonRegion,
|
||||
PhotonRoomId = photonRoomId,
|
||||
Name = dormInstanceName,
|
||||
MaxCapacity = personal.MaxCapacity,
|
||||
IsFull = false,
|
||||
IsPrivate = true,
|
||||
IsInProgress = true,
|
||||
DataBlob = dataBlob ?? string.Empty,
|
||||
EventId = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> BuildDormInstanceNameAsync(long ownerPlayerId)
|
||||
{
|
||||
var player = await db.Players.AsNoTracking()
|
||||
.Where(p => p.Id == ownerPlayerId)
|
||||
.Select(p => new { p.DisplayName, p.Username })
|
||||
.FirstOrDefaultAsync();
|
||||
var name = !string.IsNullOrWhiteSpace(player?.DisplayName)
|
||||
? player!.DisplayName
|
||||
: !string.IsNullOrWhiteSpace(player?.Username)
|
||||
? player!.Username
|
||||
: $"Player{ownerPlayerId}";
|
||||
var cleaned = new string(name
|
||||
.Select(ch => char.IsLetterOrDigit(ch) || ch == '_' || ch == '-' ? ch : '_')
|
||||
.ToArray()).Trim('_');
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? $"Player{ownerPlayerId}_dorm" : $"{cleaned}_dorm";
|
||||
}
|
||||
|
||||
private async Task RecordVisitAsync(long playerId, long roomId)
|
||||
{
|
||||
var visit = await db.RoomVisits
|
||||
.FirstOrDefaultAsync(v => v.RoomId == roomId && v.PlayerId == playerId);
|
||||
var isFirst = visit is null;
|
||||
if (isFirst)
|
||||
{
|
||||
visit = new RoomVisitEntity
|
||||
{
|
||||
RoomId = roomId,
|
||||
PlayerId = playerId,
|
||||
FirstVisitAt = DateTime.UtcNow,
|
||||
};
|
||||
db.RoomVisits.Add(visit);
|
||||
}
|
||||
visit!.LastVisitAt = DateTime.UtcNow;
|
||||
visit.VisitCount += 1;
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
await db.Rooms
|
||||
.Where(r => r.Id == roomId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(r => r.VisitCount, r => r.VisitCount + 1)
|
||||
.SetProperty(r => r.VisitorCount, r => r.VisitorCount + 1)
|
||||
.SetProperty(r => r.HotScore, r => r.HotScore + 2.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
await db.Rooms
|
||||
.Where(r => r.Id == roomId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(r => r.VisitCount, r => r.VisitCount + 1)
|
||||
.SetProperty(r => r.HotScore, r => r.HotScore + 0.25));
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>PUT <c>/roominstance/{id}/inprogress</c> — fired by
|
||||
/// <c>RecNet.Matchmaking.SetGameInProgress(bool inProgress)</c>
|
||||
|
||||
@@ -38,6 +38,25 @@ public class MatchPlayerController(
|
||||
[HttpPost("/player/login")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
|
||||
=> await RecordLoginLockAsync(loginLock, "player-login");
|
||||
|
||||
/// <summary>
|
||||
/// POST <c>/player/exclusivelogin</c> — newer clients use this instead
|
||||
/// of <c>/player/login</c> and include a
|
||||
/// <c>TakeOverExclusiveSession</c> form value. The lock semantics are
|
||||
/// otherwise the same: the latest LoginLock wins for this account.
|
||||
/// </summary>
|
||||
[HttpPost("/player/exclusivelogin")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> PlayerExclusiveLogin(
|
||||
[FromForm(Name = "LoginLock")] string? loginLock,
|
||||
[FromForm(Name = "TakeOverExclusiveSession")] bool? takeOverExclusiveSession)
|
||||
=> await RecordLoginLockAsync(loginLock, "player-exclusive-login", takeOverExclusiveSession);
|
||||
|
||||
private async Task<IActionResult> RecordLoginLockAsync(
|
||||
string? loginLock,
|
||||
string logScope,
|
||||
bool? takeOverExclusiveSession = null)
|
||||
{
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
||||
@@ -46,8 +65,8 @@ public class MatchPlayerController(
|
||||
if (replaced)
|
||||
{
|
||||
log.LogWarning(
|
||||
"[player-login] concurrent session for player={PlayerId}; kicking old session(s) via ModerationKick push",
|
||||
playerId);
|
||||
"[{Scope}] concurrent session for player={PlayerId}; takeOver={TakeOver}; kicking old session(s) via ModerationKick push",
|
||||
logScope, playerId, takeOverExclusiveSession);
|
||||
// The new session hasn't opened its SignalR socket yet
|
||||
// (negotiate fires AFTER /player/login on the watch's
|
||||
// boot sequence — verified in the live trace), so the
|
||||
|
||||
Reference in New Issue
Block a user