From 46d62a1c254a37f3a11c25f4e2f702f18cb788b8 Mon Sep 17 00:00:00 2001
From: Lapis <47952866+LapisGit@users.noreply.github.com>
Date: Sat, 4 Apr 2026 11:36:23 -0400
Subject: [PATCH] remove old instances if no heartbeat is sent to it after a
bit
---
...AddPlayerCountAndLastHeartbeat.Designer.cs | 702 ++++++++++++++++++
...04153303_AddPlayerCountAndLastHeartbeat.cs | 41 +
.../Migrations/AppDbContextModelSnapshot.cs | 6 +
CannedNet/Services/MatchmakingService.cs | 17 +
4 files changed, 766 insertions(+)
create mode 100644 CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.Designer.cs
create mode 100644 CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.cs
diff --git a/CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.Designer.cs b/CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.Designer.cs
new file mode 100644
index 0000000..e0c8366
--- /dev/null
+++ b/CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.Designer.cs
@@ -0,0 +1,702 @@
+//
+using System;
+using CannedNet.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace CannedNet.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260404153303_AddPlayerCountAndLastHeartbeat")]
+ partial class AddPlayerCountAndLastHeartbeat
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CannedNet.Account", b =>
+ {
+ b.Property("AccountId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "accountId");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasAnnotation("Relational:JsonPropertyName", "createdAt");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "displayName");
+
+ b.Property("IdentityFlags")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "identityFlags");
+
+ b.Property("IsJunior")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isJunior");
+
+ b.Property("PersonalPronouns")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "personalPronouns");
+
+ b.Property("Platforms")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "platforms");
+
+ b.Property("ProfileImage")
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "profileImage");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "username");
+
+ b.HasKey("AccountId");
+
+ b.ToTable("accounts", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.AvatarItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AvatarItemDesc")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "avatarItemDesc");
+
+ b.Property("AvatarItemType")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "avatarItemType");
+
+ b.Property("FriendlyName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "friendlyName");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("PlatformMask")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "platformMask");
+
+ b.Property("Rarity")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "rarity");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "tooltip");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId");
+
+ b.ToTable("avatar_items", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.CachedLogin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AccountId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "accountId");
+
+ b.Property("LastLoginTime")
+ .HasColumnType("timestamp with time zone")
+ .HasAnnotation("Relational:JsonPropertyName", "lastLoginTime");
+
+ b.Property("Platform")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "platform");
+
+ b.Property("PlatformID")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "platformId");
+
+ b.Property("RequirePassword")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "requirePassword");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Platform", "PlatformID")
+ .IsUnique();
+
+ b.ToTable("cached_logins", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.LoadScreen", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ImageUrl")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "imageUrl");
+
+ b.Property("IsThumbnail")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isThumbnail");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "tooltip");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("load_screens", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerAvatar", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FaceFeatures")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "FaceFeatures");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "HairColor");
+
+ b.Property("OutfitSelections")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "OutfitSelections");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "SkinColor");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId")
+ .IsUnique();
+
+ b.ToTable("player_avatars", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerProgression", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Level")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "level");
+
+ b.Property("PlayerId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "playerId");
+
+ b.Property("Xp")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "xp");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PlayerId")
+ .IsUnique();
+
+ b.ToTable("player_progressions", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PlayerSetting", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "key");
+
+ b.Property("PlayerId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "playerId");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "value");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PlayerId", "Key")
+ .IsUnique();
+
+ b.ToTable("player_settings", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PromoExternalContent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "tooltip");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "type");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "url");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("promo_external_contents", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.PromoImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ImageUrl")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "imageUrl");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "sortOrder");
+
+ b.Property("Tooltip")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "tooltip");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("promo_images", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.Room", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer");
+
+ b.Property("Accessibility")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "accessibility");
+
+ b.Property("CloningAllowed")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "cloningAllowed");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasAnnotation("Relational:JsonPropertyName", "createdAt");
+
+ b.Property("CreatorAccountId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "creatorAccountId");
+
+ b.Property("CustomWarning")
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "customWarning");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "description");
+
+ b.Property("DisableMicAutoMute")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "disableMicAutoMute");
+
+ b.Property("DisableRoomComments")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "disableRoomComments");
+
+ b.Property("EncryptVoiceChat")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "encryptVoiceChat");
+
+ b.Property("ImageName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "imageName");
+
+ b.Property("IsDorm")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isDorm");
+
+ b.Property("IsRRO")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isRRO");
+
+ b.Property("MinLevel")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "minLevel");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "name");
+
+ b.Property("RoomId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "roomId");
+
+ b.Property("State")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "state");
+
+ b.Property("SupportsJuniors")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsJuniors");
+
+ b.Property("SupportsLevelVoting")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsLevelVoting");
+
+ b.Property("SupportsMobile")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsMobile");
+
+ b.Property("SupportsQuest2")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsQuest2");
+
+ b.Property("SupportsScreens")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsScreens");
+
+ b.Property("SupportsTeleportVR")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsTeleportVR");
+
+ b.Property("SupportsVRLow")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsVRLow");
+
+ b.Property("SupportsWalkVR")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "supportsWalkVR");
+
+ b.Property("Tags")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "tags");
+
+ b.Property("WarningMask")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "warningMask");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatorAccountId");
+
+ b.ToTable("rooms", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.RoomInstance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("EncryptVoiceChat")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "EncryptVoiceChat");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("clubId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "clubId");
+
+ b.Property("dataBlob")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "dataBlob");
+
+ b.Property("eventId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "eventId");
+
+ b.Property("isFull")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isFull");
+
+ b.Property("isInProgress")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isInProgress");
+
+ b.Property("isPrivate")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isPrivate");
+
+ b.Property("lastHeartbeat")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("location")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "location");
+
+ b.Property("maxCapacity")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "maxCapacity");
+
+ b.Property("name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "name");
+
+ b.Property("pendingJoin")
+ .HasColumnType("boolean");
+
+ b.Property("photonRegionId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "photonRegionId");
+
+ b.Property("photonRoomId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "photonRoomId");
+
+ b.Property("playerCount")
+ .HasColumnType("integer");
+
+ b.Property("roomCode")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "roomCode");
+
+ b.Property("roomId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "roomId");
+
+ b.Property("roomInstanceId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "roomInstanceId");
+
+ b.Property("roomInstanceType")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "roomInstanceType");
+
+ b.Property("subRoomId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "subRoomId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId", "roomId");
+
+ b.ToTable("room_instances", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.RoomRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AccountId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "accountId");
+
+ b.Property("InvitedRole")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "invitedRole");
+
+ b.Property("Role")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "role");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("room_roles", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.SavedOutfit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FaceFeatures")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "FaceFeatures");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "HairColor");
+
+ b.Property("OutfitSelections")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "OutfitSelections");
+
+ b.Property("OwnerAccountId")
+ .HasColumnType("integer");
+
+ b.Property("PreviewImageName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "PreviewImageName");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "SkinColor");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "Slot");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerAccountId")
+ .IsUnique();
+
+ b.ToTable("saved_outfits", (string)null);
+ });
+
+ modelBuilder.Entity("CannedNet.SubRoom", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Accessibility")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "accessibility");
+
+ b.Property("DataBlob")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "dataBlob");
+
+ b.Property("DataSavedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasAnnotation("Relational:JsonPropertyName", "dataSavedAt");
+
+ b.Property("IsSandbox")
+ .HasColumnType("boolean")
+ .HasAnnotation("Relational:JsonPropertyName", "isSandbox");
+
+ b.Property("MaxPlayers")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "maxPlayers");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "name");
+
+ b.Property("RoomId")
+ .HasColumnType("integer");
+
+ b.Property("SubRoomId")
+ .HasColumnType("integer")
+ .HasAnnotation("Relational:JsonPropertyName", "subRoomId");
+
+ b.Property("UnitySceneId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasAnnotation("Relational:JsonPropertyName", "unitySceneId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomId");
+
+ b.ToTable("sub_rooms", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.cs b/CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.cs
new file mode 100644
index 0000000..c728973
--- /dev/null
+++ b/CannedNet/Migrations/20260404153303_AddPlayerCountAndLastHeartbeat.cs
@@ -0,0 +1,41 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CannedNet.Migrations
+{
+ ///
+ public partial class AddPlayerCountAndLastHeartbeat : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "lastHeartbeat",
+ table: "room_instances",
+ type: "timestamp with time zone",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.AddColumn(
+ name: "playerCount",
+ table: "room_instances",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "lastHeartbeat",
+ table: "room_instances");
+
+ migrationBuilder.DropColumn(
+ name: "playerCount",
+ table: "room_instances");
+ }
+ }
+}
diff --git a/CannedNet/Migrations/AppDbContextModelSnapshot.cs b/CannedNet/Migrations/AppDbContextModelSnapshot.cs
index 031b2a2..c18abc8 100644
--- a/CannedNet/Migrations/AppDbContextModelSnapshot.cs
+++ b/CannedNet/Migrations/AppDbContextModelSnapshot.cs
@@ -502,6 +502,9 @@ namespace CannedNet.Migrations
.HasColumnType("boolean")
.HasAnnotation("Relational:JsonPropertyName", "isPrivate");
+ b.Property("lastHeartbeat")
+ .HasColumnType("timestamp with time zone");
+
b.Property("location")
.IsRequired()
.HasColumnType("text")
@@ -529,6 +532,9 @@ namespace CannedNet.Migrations
.HasColumnType("text")
.HasAnnotation("Relational:JsonPropertyName", "photonRoomId");
+ b.Property("playerCount")
+ .HasColumnType("integer");
+
b.Property("roomCode")
.IsRequired()
.HasColumnType("text")
diff --git a/CannedNet/Services/MatchmakingService.cs b/CannedNet/Services/MatchmakingService.cs
index f12bfcc..be6e0a2 100644
--- a/CannedNet/Services/MatchmakingService.cs
+++ b/CannedNet/Services/MatchmakingService.cs
@@ -244,6 +244,23 @@ public class MatchmakingService
.OrderByDescending(r => r.Id)
.FirstOrDefaultAsync();
+ if (roomInstance != null)
+ {
+ roomInstance.lastHeartbeat = DateTime.UtcNow;
+ roomInstance.playerCount = Math.Max(roomInstance.playerCount, 1);
+ }
+
+ var oldInstances = await db.RoomInstances
+ .Where(r => r.lastHeartbeat < DateTime.UtcNow.AddMinutes(-5) && r.playerCount <= 0)
+ .ToListAsync();
+
+ foreach (var old in oldInstances)
+ {
+ db.RoomInstances.Remove(old);
+ }
+
+ await db.SaveChangesAsync();
+
return Results.Json(new
{
playerId = heartbeat.playerId != 0 ? heartbeat.playerId : id,