mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24cc13084a | |||
| 193d9b343c | |||
| 97f6522497 | |||
| e3bbcbe4a4 |
@@ -53,12 +53,14 @@ WORKDIR /src
|
||||
# DorkNet.Models project ref, so its csproj needs to be present too.
|
||||
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
||||
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
||||
COPY DorkNet.Contracts/DorkNet.Contracts.csproj DorkNet.Contracts/
|
||||
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
||||
|
||||
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
||||
# SPA into wwwroot/admin so the publish step ships it as static content.
|
||||
COPY DorkNet.Server/ DorkNet.Server/
|
||||
COPY DorkNet.Models/ DorkNet.Models/
|
||||
COPY DorkNet.Contracts/ DorkNet.Contracts/
|
||||
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||
|
||||
|
||||
@@ -167,6 +167,11 @@ public static class DorkNetRouteOwnership
|
||||
public static IReadOnlyList<string> PublicSubdomains =>
|
||||
ServiceRouteGroups
|
||||
.SelectMany(group => group.HostSubdomains)
|
||||
// api.* is a shared public host: it owns no service by host and is
|
||||
// dispatched by path (see ResolvePublicService's path-prefix pass),
|
||||
// so it never appears in a group's HostSubdomains. It must still be
|
||||
// whitelisted here or HostFiltering rejects every api.{apex} request.
|
||||
.Append("api")
|
||||
.Append(AdminHost)
|
||||
.Append("feed")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace DorkNet.Server.Controllers.Match;
|
||||
[ApiController]
|
||||
public class MatchPlayerController(
|
||||
PlayerPresenceService presence,
|
||||
NotificationService notifications,
|
||||
RoomService rooms,
|
||||
PrivateInstanceService privateInstances,
|
||||
IConfiguration config,
|
||||
@@ -37,7 +36,7 @@ public class MatchPlayerController(
|
||||
/// </summary>
|
||||
[HttpPost("/player/login")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
public async Task<IActionResult> PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
|
||||
public IActionResult PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
|
||||
{
|
||||
var playerId = TryGetCurrentPlayerId();
|
||||
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
||||
@@ -46,17 +45,15 @@ public class MatchPlayerController(
|
||||
if (replaced)
|
||||
{
|
||||
log.LogWarning(
|
||||
"[player-login] concurrent session for player={PlayerId}; kicking old session(s) via ModerationKick push",
|
||||
"[player-login] concurrent session marker replaced for player={PlayerId}; stale SignalR connections will be evicted on hub connect",
|
||||
playerId);
|
||||
// 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
|
||||
// ModerationKick push hits only the OLDER session's
|
||||
// existing connections. They'll show the "you've been
|
||||
// kicked" dialog and gracefully disconnect via the
|
||||
// watch's LocalModerationKickSelf path.
|
||||
await notifications.KickStaleSession(playerId,
|
||||
"Your account was signed in on another device.");
|
||||
// Do not broadcast ModerationKick to the whole player group
|
||||
// here. During reconnects the active watch can already have a
|
||||
// SignalR connection in that group, and the 2020 client crashes
|
||||
// hard if it receives a malformed/stale moderation payload.
|
||||
// NotifyHub.OnConnectedAsync snapshots existing connection ids
|
||||
// before adding the new connection and sends a targeted kick to
|
||||
// those ids only.
|
||||
}
|
||||
}
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
||||
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Service-slice containers reference DorkNet.Server for shared controllers
|
||||
|
||||
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
|
||||
// in, so we save once with a placeholder, then patch and save
|
||||
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
||||
PhotonRoomName = "pending",
|
||||
MaxCapacity = 8,
|
||||
// Track the room's "Max Player Count in This Subroom" slider
|
||||
// instead of a hardcoded 8, so matchmade joiners fill the room to
|
||||
// the host's chosen limit rather than fragmenting into a new
|
||||
// instance every 8 players. (Existing sessions keep whatever cap
|
||||
// they were created with.)
|
||||
MaxCapacity = await ResolveRoomMaxCapacityAsync(effectiveRoom),
|
||||
PlayerCount = 1,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
|
||||
return ToDto(session);
|
||||
}
|
||||
|
||||
// Default session cap when we can't resolve a room-specific value (system
|
||||
// rooms like the dorm use a GUID roomId that doesn't map to a RoomEntity).
|
||||
private const int DefaultMaxCapacity = 8;
|
||||
|
||||
/// <summary>Resolve a new session's capacity from the room's entry
|
||||
/// subroom (lowest <c>OrderIndex</c>) <see cref="RoomSceneEntity.MaxPlayers"/>
|
||||
/// — the value the in-game "Max Player Count in This Subroom" slider
|
||||
/// writes. Matchmade sessions land in the entry subroom, so its cap is the
|
||||
/// right ceiling. GameSession roomIds are the room's numeric Id as a
|
||||
/// string for user rooms; non-numeric (dorm GUID, etc.) → default. Clamped
|
||||
/// to a sane Photon ceiling.</summary>
|
||||
private async Task<int> ResolveRoomMaxCapacityAsync(string roomId)
|
||||
{
|
||||
if (!long.TryParse(roomId, out var roomLongId)) return DefaultMaxCapacity;
|
||||
var max = await db.RoomScenes
|
||||
.Where(s => s.RoomId == roomLongId)
|
||||
.OrderBy(s => s.OrderIndex)
|
||||
.Select(s => (int?)s.MaxPlayers)
|
||||
.FirstOrDefaultAsync();
|
||||
return max is int v && v > 0 ? Math.Clamp(v, 1, 80) : DefaultMaxCapacity;
|
||||
}
|
||||
|
||||
public async Task<GameSession?> GetByIdAsync(long id)
|
||||
{
|
||||
var s = await db.GameSessions.AsNoTracking()
|
||||
|
||||
@@ -17,8 +17,9 @@ x-dorknet-service-env: &dorknet-service-env
|
||||
|
||||
x-dorknet-app-networks: &dorknet-app-networks
|
||||
networks:
|
||||
# Internal only — backends are reached by the gateway over `default`,
|
||||
# never directly by Traefik, so keep them off dokploy-network.
|
||||
- default
|
||||
- dokploy-network
|
||||
|
||||
services:
|
||||
gateway:
|
||||
@@ -42,6 +43,18 @@ services:
|
||||
DorkNet__Services__Monolith: http://monolith:8080
|
||||
expose:
|
||||
- "8080"
|
||||
networks:
|
||||
- default
|
||||
- dokploy-network
|
||||
labels:
|
||||
# Cloudflare terminates TLS at the edge; the tunnel -> Traefik leg is
|
||||
# internal HTTP, so route on the `web` entrypoint with no certresolver.
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=dokploy-network
|
||||
# Scope routing to YOUR domain (apex + subdomains) instead of catch-all:
|
||||
- traefik.http.routers.dorknet-gw.rule=Host(`${DORKNET_DOMAIN}`) || HostRegexp(`^.+\.${DORKNET_DOMAIN}$`)
|
||||
- traefik.http.routers.dorknet-gw.entrypoints=web
|
||||
- traefik.http.services.dorknet-gw.loadbalancer.server.port=8080
|
||||
depends_on:
|
||||
- identity
|
||||
- rooms
|
||||
@@ -57,6 +70,10 @@ services:
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:?Set CLOUDFLARE_TUNNEL_TOKEN in Dokploy}
|
||||
# Must share dokploy-network so the tunnel can reach the Traefik
|
||||
# container (dokploy-traefik) by name and hand traffic off to it.
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
- gateway
|
||||
restart: unless-stopped
|
||||
|
||||
Reference in New Issue
Block a user