From 54953fbd6c1ffbd9737298036c500f7c650cbe7e Mon Sep 17 00:00:00 2001 From: CyberL1 Date: Sat, 29 Jun 2024 21:34:42 +0200 Subject: [PATCH] feat(server): snapshot worlds --- Minecraft-Realms-Emulator/Entities/World.cs | 1 + .../Middlewares/CheckRealmOwnerMiddleware.cs | 7 +- ...20240629161935_Snapshot_Worlds.Designer.cs | 500 ++++++++++++++++++ .../20240629161935_Snapshot_Worlds.cs | 48 ++ .../Migrations/DataContextModelSnapshot.cs | 14 + .../Modes/External/SubscriptionsController.cs | 7 +- .../Modes/External/WorldsController.cs | 335 +++++++++++- .../Controllers/SubscriptionsController.cs | 7 +- .../Realms/Controllers/WorldsController.cs | 348 +++++++++++- .../Responses/WorldResponse.cs | 2 + 10 files changed, 1262 insertions(+), 7 deletions(-) create mode 100644 Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.Designer.cs create mode 100644 Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.cs diff --git a/Minecraft-Realms-Emulator/Entities/World.cs b/Minecraft-Realms-Emulator/Entities/World.cs index 6bd6c51..2d29781 100644 --- a/Minecraft-Realms-Emulator/Entities/World.cs +++ b/Minecraft-Realms-Emulator/Entities/World.cs @@ -18,5 +18,6 @@ public int ActiveSlot { get; set; } = 1; public List Slots { get; set; } = []; public bool Member { get; set; } = false; + public World? ParentWorld { get; set; } } } \ No newline at end of file diff --git a/Minecraft-Realms-Emulator/Middlewares/CheckRealmOwnerMiddleware.cs b/Minecraft-Realms-Emulator/Middlewares/CheckRealmOwnerMiddleware.cs index 1b0dccc..74432ae 100644 --- a/Minecraft-Realms-Emulator/Middlewares/CheckRealmOwnerMiddleware.cs +++ b/Minecraft-Realms-Emulator/Middlewares/CheckRealmOwnerMiddleware.cs @@ -1,4 +1,5 @@ -using Minecraft_Realms_Emulator.Attributes; +using Microsoft.EntityFrameworkCore; +using Minecraft_Realms_Emulator.Attributes; using Minecraft_Realms_Emulator.Data; using Minecraft_Realms_Emulator.Entities; @@ -20,9 +21,9 @@ namespace Minecraft_Realms_Emulator.Middlewares } string playerUUID = httpContext.Request.Headers.Cookie.ToString().Split(";")[0].Split(":")[2]; - World world = db.Worlds.Find(int.Parse(httpContext.Request.RouteValues["wId"].ToString())); + World world = db.Worlds.Include(w => w.ParentWorld).FirstOrDefault(w => w.Id == int.Parse(httpContext.Request.RouteValues["wId"].ToString())); - if (world != null && !attribute.IsRealmOwner(playerUUID, world.OwnerUUID)) + if (world != null && !attribute.IsRealmOwner(playerUUID, world.ParentWorld == null ? world.OwnerUUID : world.ParentWorld.OwnerUUID)) { httpContext.Response.StatusCode = 403; await httpContext.Response.WriteAsync("You don't own this world"); diff --git a/Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.Designer.cs b/Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.Designer.cs new file mode 100644 index 0000000..dcbe2ab --- /dev/null +++ b/Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.Designer.cs @@ -0,0 +1,500 @@ +// +using System; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Minecraft_Realms_Emulator.Data; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Minecraft_Realms_Emulator.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240629161935_Snapshot_Worlds")] + partial class Snapshot_Worlds + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Backup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BackupId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModifiedDate") + .HasColumnType("bigint"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Backups"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Configuration", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Key"); + + b.ToTable("Configuration"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Connection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("PendingUpdate") + .HasColumnType("boolean"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Connections"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("InvitationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipeintUUID") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Invites"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ButtonText") + .HasColumnType("jsonb"); + + b.Property("Dismissable") + .HasColumnType("boolean"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("NotificationUuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("jsonb"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UrlButton") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Accepted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Online") + .HasColumnType("boolean"); + + b.Property("Operator") + .HasColumnType("boolean"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("Uuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.SeenNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("NotificationUUID") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerUUID") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SeenNotifications"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Slot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommandBlocks") + .HasColumnType("boolean"); + + b.Property("Difficulty") + .HasColumnType("integer"); + + b.Property("ForceGameMode") + .HasColumnType("boolean"); + + b.Property("GameMode") + .HasColumnType("integer"); + + b.Property("Pvp") + .HasColumnType("boolean"); + + b.Property("SlotId") + .HasColumnType("integer"); + + b.Property("SlotName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SpawnAnimals") + .HasColumnType("boolean"); + + b.Property("SpawnMonsters") + .HasColumnType("boolean"); + + b.Property("SpawnNPCs") + .HasColumnType("boolean"); + + b.Property("SpawnProtection") + .HasColumnType("integer"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("Slots"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriptionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId") + .IsUnique(); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Link") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecommendedPlayers") + .IsRequired() + .HasColumnType("text"); + + b.Property("Trailer") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveSlot") + .HasColumnType("integer"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Member") + .HasColumnType("boolean"); + + b.Property("MinigameId") + .HasColumnType("integer"); + + b.Property("MinigameImage") + .HasColumnType("text"); + + b.Property("MinigameName") + .HasColumnType("text"); + + b.Property("Motd") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Owner") + .HasColumnType("text"); + + b.Property("OwnerUUID") + .HasColumnType("text"); + + b.Property("ParentWorldId") + .HasColumnType("integer"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorldType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ParentWorldId"); + + b.ToTable("Worlds"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Backup", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany() + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Connection", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany() + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Invite", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany() + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Player", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany("Players") + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Slot", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithMany("Slots") + .HasForeignKey("WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.Subscription", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "World") + .WithOne("Subscription") + .HasForeignKey("Minecraft_Realms_Emulator.Entities.Subscription", "WorldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("World"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "ParentWorld") + .WithMany() + .HasForeignKey("ParentWorldId"); + + b.Navigation("ParentWorld"); + }); + + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.Navigation("Players"); + + b.Navigation("Slots"); + + b.Navigation("Subscription"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.cs b/Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.cs new file mode 100644 index 0000000..860ed8e --- /dev/null +++ b/Minecraft-Realms-Emulator/Migrations/20240629161935_Snapshot_Worlds.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Minecraft_Realms_Emulator.Migrations +{ + /// + public partial class Snapshot_Worlds : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ParentWorldId", + table: "Worlds", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Worlds_ParentWorldId", + table: "Worlds", + column: "ParentWorldId"); + + migrationBuilder.AddForeignKey( + name: "FK_Worlds_Worlds_ParentWorldId", + table: "Worlds", + column: "ParentWorldId", + principalTable: "Worlds", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Worlds_Worlds_ParentWorldId", + table: "Worlds"); + + migrationBuilder.DropIndex( + name: "IX_Worlds_ParentWorldId", + table: "Worlds"); + + migrationBuilder.DropColumn( + name: "ParentWorldId", + table: "Worlds"); + } + } +} diff --git a/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs b/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs index b1ebfd4..bff3f50 100644 --- a/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs +++ b/Minecraft-Realms-Emulator/Migrations/DataContextModelSnapshot.cs @@ -390,6 +390,9 @@ namespace Minecraft_Realms_Emulator.Migrations b.Property("OwnerUUID") .HasColumnType("text"); + b.Property("ParentWorldId") + .HasColumnType("integer"); + b.Property("State") .IsRequired() .HasColumnType("text"); @@ -400,6 +403,8 @@ namespace Minecraft_Realms_Emulator.Migrations b.HasKey("Id"); + b.HasIndex("ParentWorldId"); + b.ToTable("Worlds"); }); @@ -469,6 +474,15 @@ namespace Minecraft_Realms_Emulator.Migrations b.Navigation("World"); }); + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => + { + b.HasOne("Minecraft_Realms_Emulator.Entities.World", "ParentWorld") + .WithMany() + .HasForeignKey("ParentWorldId"); + + b.Navigation("ParentWorld"); + }); + modelBuilder.Entity("Minecraft_Realms_Emulator.Entities.World", b => { b.Navigation("Players"); diff --git a/Minecraft-Realms-Emulator/Modes/External/SubscriptionsController.cs b/Minecraft-Realms-Emulator/Modes/External/SubscriptionsController.cs index 3ff541e..2132304 100644 --- a/Minecraft-Realms-Emulator/Modes/External/SubscriptionsController.cs +++ b/Minecraft-Realms-Emulator/Modes/External/SubscriptionsController.cs @@ -22,7 +22,12 @@ namespace Minecraft_Realms_Emulator.Modes.External [CheckRealmOwner] public async Task> Get(int wId) { - var world = await _context.Worlds.Include(w => w.Subscription).FirstOrDefaultAsync(w => w.Id == wId); + var world = await _context.Worlds.Include(w => w.Subscription).Include(w => w.ParentWorld.Subscription).FirstOrDefaultAsync(w => w.Id == wId); + + if (world.ParentWorld != null) + { + world.Subscription = world.ParentWorld.Subscription; + } if (world?.Subscription == null) return NotFound("Subscription not found"); diff --git a/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs b/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs index 77ed285..27a23fa 100644 --- a/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs +++ b/Minecraft-Realms-Emulator/Modes/External/WorldsController.cs @@ -139,6 +139,246 @@ namespace Minecraft_Realms_Emulator.Modes.External return Ok(servers); } + [HttpGet("listUserWorldsOfType/any")] + public async Task> GetWorldsSnapshot() + { + string cookie = Request.Headers.Cookie; + + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + string playerName = cookie.Split(";")[1].Split("=")[1]; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + var ownedWorlds = await _context.Worlds.Where(w => w.OwnerUUID == playerUUID || w.ParentWorld.OwnerUUID == playerUUID).Include(w => w.Subscription).Include(w => w.Slots).Include(w => w.ParentWorld).ToListAsync(); + var memberWorlds = await _context.Players.Where(p => p.Uuid == playerUUID && p.Accepted).Include(p => p.World.Subscription).Include(p => p.World.Slots).Include(p => p.World.ParentWorld).Select(p => p.World).ToListAsync(); + + List allWorlds = []; + + foreach (var world in ownedWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot?.Version ?? gameVersion)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + ActiveVersion = activeSlot?.Version ?? gameVersion, + Compatibility = isCompatible + }; + + if (world.Subscription != null) + { + response.DaysLeft = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days; + response.Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0; + response.ExpiredTrial = false; + } + + if (world.ParentWorld == null) + { + response.ParentWorldId = -1; + } + + if (world.ParentWorld != null) + { + response.Owner = world.ParentWorld.Owner; + response.OwnerUUID = world.ParentWorld.OwnerUUID; + response.ParentWorldId = world.ParentWorld.Id; + response.ParentWorldName = world.ParentWorld.Name; + } + + allWorlds.Add(response); + } + + foreach (var world in memberWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot.Version)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + DaysLeft = 0, + Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0, + ExpiredTrial = false, + ActiveVersion = activeSlot.Version, + Compatibility = isCompatible + }; + + if (world.ParentWorld == null) + { + response.ParentWorldId = -1; + } + + if (world.ParentWorld != null) + { + response.Owner = world.ParentWorld.Owner; + response.OwnerUUID = world.ParentWorld.OwnerUUID; + response.ParentWorldId = world.ParentWorld.Id; + response.ParentWorldName = world.ParentWorld.Name; + } + + allWorlds.Add(response); + } + + ServersResponse servers = new() + { + Servers = allWorlds + }; + + return Ok(servers); + } + + [HttpGet("listPrereleaseEligibleWorlds")] + public async Task> GetPrereleaseWorlds() + { + string cookie = Request.Headers.Cookie; + + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + string playerName = cookie.Split(";")[1].Split("=")[1]; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + var ownedWorlds = await _context.Worlds.Where(w => w.ParentWorld != null && w.ParentWorld.OwnerUUID == playerUUID).Include(w => w.Subscription).Include(w => w.Slots).Include(w => w.ParentWorld).ToListAsync(); + var memberWorlds = await _context.Players.Where(p => p.World.ParentWorld != null && p.Uuid == playerUUID && p.Accepted).Include(p => p.World.Subscription).Include(p => p.World.Slots).Include(p => p.World.ParentWorld).Select(p => p.World).ToListAsync(); + + List allWorlds = []; + + if (ownedWorlds.ToArray().Length == 0 && new ConfigHelper(_context).GetSetting(nameof(SettingsEnum.AutomaticRealmsCreation)).Value) + { + var parentWorld = _context.Worlds.FirstOrDefault(w => w.OwnerUUID == playerUUID && w.ParentWorld == null); + + if (parentWorld != null && parentWorld.State != nameof(StateEnum.UNINITIALIZED)) + { + var world = new World + { + Name = parentWorld.Name, + Motd = null, + State = nameof(StateEnum.UNINITIALIZED), + WorldType = nameof(WorldTypeEnum.NORMAL), + MaxPlayers = 10, + MinigameId = null, + MinigameName = null, + MinigameImage = null, + ActiveSlot = 1, + Member = false, + ParentWorld = parentWorld, + }; + + ownedWorlds.Add(world); + _context.Worlds.Add(world); + + _context.SaveChanges(); + } + } + + foreach (var world in ownedWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot?.Version ?? gameVersion)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + ActiveVersion = activeSlot?.Version ?? gameVersion, + Compatibility = isCompatible, + ParentWorldId = world.ParentWorld.Id, + ParentWorldName = world.ParentWorld.Name, + }; + + if (world.Subscription != null) + { + response.DaysLeft = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days; + response.Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0; + response.ExpiredTrial = false; + } + + allWorlds.Add(response); + } + + foreach (var world in memberWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot.Version)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + DaysLeft = 0, + Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0, + ExpiredTrial = false, + ActiveVersion = activeSlot.Version, + Compatibility = isCompatible + }; + + allWorlds.Add(response); + } + + ServersResponse servers = new() + { + Servers = allWorlds + }; + + return Ok(servers); + } + [HttpGet("{wId}")] [CheckForWorld] [CheckRealmOwner] @@ -147,7 +387,7 @@ namespace Minecraft_Realms_Emulator.Modes.External string cookie = Request.Headers.Cookie; string gameVersion = cookie.Split(";")[2].Split("=")[1]; - var world = await _context.Worlds.Include(w => w.Players).Include(w => w.Subscription).Include(w => w.Slots).FirstOrDefaultAsync(w => w.Id == wId); + var world = await _context.Worlds.Include(w => w.Players).Include(w => w.Subscription).Include(w => w.Slots).Include(w => w.ParentWorld.Subscription).FirstOrDefaultAsync(w => w.Id == wId); Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); @@ -181,6 +421,11 @@ namespace Minecraft_Realms_Emulator.Modes.External var activeSlotOptions = JsonConvert.DeserializeObject(slots.Find(s => s.SlotId == activeSlot.SlotId).Options); + if (world.ParentWorld != null) + { + world.Subscription = world.ParentWorld.Subscription; + } + WorldResponse response = new() { Id = world.Id, @@ -279,6 +524,80 @@ namespace Minecraft_Realms_Emulator.Modes.External return Ok(world); } + [HttpPost("{wId}/createPrereleaseRealm")] + [CheckForWorld] + [CheckRealmOwner] + public async Task> CreatePrereleaseRealms(int wId) + { + string cookie = Request.Headers.Cookie; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + var worlds = await _context.Worlds.ToListAsync(); + + var world = worlds.Find(w => w.Id == wId); + + if (world.ParentWorld.State == nameof(StateEnum.UNINITIALIZED)) + { + ErrorResponse errorResponse = new() + { + ErrorCode = 401, + ErrorMsg = "You must initialize release world first", + }; + + return StatusCode(401, errorResponse); + } + + if (world.State != nameof(StateEnum.UNINITIALIZED)) + { + ErrorResponse errorResponse = new() + { + ErrorCode = 401, + ErrorMsg = "A prerealease realm is already created for this world", + }; + + return StatusCode(401, errorResponse); + } + + world.Name = $"[PRE] {world.ParentWorld.Name}"; + world.Motd = $"[PRE] {world.ParentWorld.Motd}"; + world.State = nameof(StateEnum.OPEN); + + var config = new ConfigHelper(_context); + var defaultServerAddress = config.GetSetting(nameof(SettingsEnum.DefaultServerAddress)); + + var connection = new Connection + { + World = world, + Address = defaultServerAddress.Value + }; + + Slot slot = new() + { + World = world, + SlotId = 1, + SlotName = "", + Version = gameVersion, + GameMode = 0, + Difficulty = 2, + SpawnProtection = 0, + ForceGameMode = false, + Pvp = true, + SpawnAnimals = true, + SpawnMonsters = true, + SpawnNPCs = true, + CommandBlocks = false + }; + + _context.Worlds.Update(world); + + _context.Connections.Add(connection); + _context.Slots.Add(slot); + + _context.SaveChanges(); + + return Ok(world); + } + [HttpPost("{wId}/reset")] [CheckForWorld] [CheckRealmOwner] @@ -464,6 +783,20 @@ namespace Minecraft_Realms_Emulator.Modes.External { var world = _context.Worlds.Find(wId); + if (world.ParentWorld == null) + { + var snapshotWorld = _context.Worlds.FirstOrDefault(w => w.ParentWorld.Id == wId); + + if (snapshotWorld != null) + { + _context.Worlds.Remove(snapshotWorld); + } + } + else + { + _context.Worlds.Remove(world.ParentWorld); + } + _context.Worlds.Remove(world); _context.SaveChanges(); diff --git a/Minecraft-Realms-Emulator/Modes/Realms/Controllers/SubscriptionsController.cs b/Minecraft-Realms-Emulator/Modes/Realms/Controllers/SubscriptionsController.cs index f5bbddc..eaac54d 100644 --- a/Minecraft-Realms-Emulator/Modes/Realms/Controllers/SubscriptionsController.cs +++ b/Minecraft-Realms-Emulator/Modes/Realms/Controllers/SubscriptionsController.cs @@ -22,7 +22,12 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers [CheckRealmOwner] public async Task> Get(int wId) { - var world = await _context.Worlds.Include(w => w.Subscription).FirstOrDefaultAsync(w => w.Id == wId); + var world = await _context.Worlds.Include(w => w.Subscription).Include(w => w.ParentWorld.Subscription).FirstOrDefaultAsync(w => w.Id == wId); + + if (world.ParentWorld != null) + { + world.Subscription = world.ParentWorld.Subscription; + } if (world?.Subscription == null) return NotFound("Subscription not found"); diff --git a/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs b/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs index 361df93..cababc2 100644 --- a/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs +++ b/Minecraft-Realms-Emulator/Modes/Realms/Controllers/WorldsController.cs @@ -142,6 +142,246 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers return Ok(servers); } + [HttpGet("listUserWorldsOfType/any")] + public async Task> GetWorldsSnapshot() + { + string cookie = Request.Headers.Cookie; + + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + string playerName = cookie.Split(";")[1].Split("=")[1]; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + var ownedWorlds = await _context.Worlds.Where(w => w.OwnerUUID == playerUUID || w.ParentWorld.OwnerUUID == playerUUID).Include(w => w.Subscription).Include(w => w.Slots).Include(w => w.ParentWorld).ToListAsync(); + var memberWorlds = await _context.Players.Where(p => p.Uuid == playerUUID && p.Accepted).Include(p => p.World.Subscription).Include(p => p.World.Slots).Include(p => p.World.ParentWorld).Select(p => p.World).ToListAsync(); + + List allWorlds = []; + + foreach (var world in ownedWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot?.Version ?? gameVersion)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + ActiveVersion = activeSlot?.Version ?? gameVersion, + Compatibility = isCompatible + }; + + if (world.Subscription != null) + { + response.DaysLeft = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days; + response.Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0; + response.ExpiredTrial = false; + } + + if (world.ParentWorld == null) + { + response.ParentWorldId = -1; + } + + if (world.ParentWorld != null) + { + response.Owner = world.ParentWorld.Owner; + response.OwnerUUID = world.ParentWorld.OwnerUUID; + response.ParentWorldId = world.ParentWorld.Id; + response.ParentWorldName = world.ParentWorld.Name; + } + + allWorlds.Add(response); + } + + foreach (var world in memberWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot.Version)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + DaysLeft = 0, + Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0, + ExpiredTrial = false, + ActiveVersion = activeSlot.Version, + Compatibility = isCompatible + }; + + if (world.ParentWorld == null) + { + response.ParentWorldId = -1; + } + + if (world.ParentWorld != null) + { + response.Owner = world.ParentWorld.Owner; + response.OwnerUUID = world.ParentWorld.OwnerUUID; + response.ParentWorldId = world.ParentWorld.Id; + response.ParentWorldName = world.ParentWorld.Name; + } + + allWorlds.Add(response); + } + + ServersResponse servers = new() + { + Servers = allWorlds + }; + + return Ok(servers); + } + + [HttpGet("listPrereleaseEligibleWorlds")] + public async Task> GetPrereleaseWorlds() + { + string cookie = Request.Headers.Cookie; + + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + string playerName = cookie.Split(";")[1].Split("=")[1]; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + var ownedWorlds = await _context.Worlds.Where(w => w.ParentWorld != null && w.ParentWorld.OwnerUUID == playerUUID).Include(w => w.Subscription).Include(w => w.Slots).Include(w => w.ParentWorld).ToListAsync(); + var memberWorlds = await _context.Players.Where(p => p.World.ParentWorld != null && p.Uuid == playerUUID && p.Accepted).Include(p => p.World.Subscription).Include(p => p.World.Slots).Include(p => p.World.ParentWorld).Select(p => p.World).ToListAsync(); + + List allWorlds = []; + + if (ownedWorlds.ToArray().Length == 0 && new ConfigHelper(_context).GetSetting(nameof(SettingsEnum.AutomaticRealmsCreation)).Value) + { + var parentWorld = _context.Worlds.FirstOrDefault(w => w.OwnerUUID == playerUUID && w.ParentWorld == null); + + if (parentWorld != null && parentWorld.State != nameof(StateEnum.UNINITIALIZED)) + { + var world = new World + { + Name = parentWorld.Name, + Motd = null, + State = nameof(StateEnum.UNINITIALIZED), + WorldType = nameof(WorldTypeEnum.NORMAL), + MaxPlayers = 10, + MinigameId = null, + MinigameName = null, + MinigameImage = null, + ActiveSlot = 1, + Member = false, + ParentWorld = parentWorld, + }; + + ownedWorlds.Add(world); + _context.Worlds.Add(world); + + _context.SaveChanges(); + } + } + + foreach (var world in ownedWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot?.Version ?? gameVersion)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + ActiveVersion = activeSlot?.Version ?? gameVersion, + Compatibility = isCompatible, + ParentWorldId = world.ParentWorld.Id, + ParentWorldName = world.ParentWorld.Name, + }; + + if (world.Subscription != null) + { + response.DaysLeft = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days; + response.Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0; + response.ExpiredTrial = false; + } + + allWorlds.Add(response); + } + + foreach (var world in memberWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = new MinecraftVersionParser.MinecraftVersion(gameVersion).CompareTo(new MinecraftVersionParser.MinecraftVersion(activeSlot.Version)); + string isCompatible = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + WorldResponse response = new() + { + Id = world.Id, + Owner = world.Owner, + OwnerUUID = world.OwnerUUID, + Name = world.Name, + Motd = world.Motd, + State = world.State, + WorldType = world.WorldType, + MaxPlayers = world.MaxPlayers, + MinigameId = world.MinigameId, + MinigameName = world.MinigameName, + MinigameImage = world.MinigameImage, + ActiveSlot = world.ActiveSlot, + Member = world.Member, + Players = world.Players, + DaysLeft = 0, + Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0, + ExpiredTrial = false, + ActiveVersion = activeSlot.Version, + Compatibility = isCompatible + }; + + allWorlds.Add(response); + } + + ServersResponse servers = new() + { + Servers = allWorlds + }; + + return Ok(servers); + } + [HttpGet("{wId}")] [CheckForWorld] [CheckRealmOwner] @@ -150,7 +390,7 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers string cookie = Request.Headers.Cookie; string gameVersion = cookie.Split(";")[2].Split("=")[1]; - var world = await _context.Worlds.Include(w => w.Players).Include(w => w.Subscription).Include(w => w.Slots).FirstOrDefaultAsync(w => w.Id == wId); + var world = await _context.Worlds.Include(w => w.Players).Include(w => w.Subscription).Include(w => w.Slots).Include(w => w.ParentWorld.Subscription).FirstOrDefaultAsync(w => w.Id == wId); Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); @@ -184,6 +424,11 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers var activeSlotOptions = JsonConvert.DeserializeObject(slots.Find(s => s.SlotId == activeSlot.SlotId).Options); + if (world.ParentWorld != null) + { + world.Subscription = world.ParentWorld.Subscription; + } + WorldResponse response = new() { Id = world.Id, @@ -294,6 +539,91 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers return Ok(world); } + [HttpPost("{wId}/createPrereleaseRealm")] + [CheckForWorld] + [CheckRealmOwner] + public async Task> CreatePrereleaseRealms(int wId) + { + string cookie = Request.Headers.Cookie; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + var worlds = await _context.Worlds.ToListAsync(); + + var world = worlds.Find(w => w.Id == wId); + + if (world.ParentWorld.State == nameof(StateEnum.UNINITIALIZED)) + { + ErrorResponse errorResponse = new() + { + ErrorCode = 401, + ErrorMsg = "You must initialize release world first", + }; + + return StatusCode(401, errorResponse); + } + + if (world.State != nameof(StateEnum.UNINITIALIZED)) + { + ErrorResponse errorResponse = new() + { + ErrorCode = 401, + ErrorMsg = "A prerealease realm is already created for this world", + }; + + return StatusCode(401, errorResponse); + } + + world.Name = $"[PRE] {world.ParentWorld.Name}"; + world.Motd = $"[PRE] {world.ParentWorld.Motd}"; + world.State = nameof(StateEnum.OPEN); + + var config = new ConfigHelper(_context); + var defaultServerAddress = config.GetSetting(nameof(SettingsEnum.DefaultServerAddress)); + + static int FindFreeTcpPort() + { + TcpListener l = new(IPAddress.Loopback, 0); + l.Start(); + int port = ((IPEndPoint)l.LocalEndpoint).Port; + l.Stop(); + return port; + } + + var port = FindFreeTcpPort(); + + var connection = new Connection + { + World = world, + Address = $"{defaultServerAddress.Value}:{port}" + }; + + Slot slot = new() + { + World = world, + SlotId = 1, + SlotName = "", + Version = gameVersion, + GameMode = 0, + Difficulty = 2, + SpawnProtection = 0, + ForceGameMode = false, + Pvp = true, + SpawnAnimals = true, + SpawnMonsters = true, + SpawnNPCs = true, + CommandBlocks = false + }; + + _context.Worlds.Update(world); + + _context.Connections.Add(connection); + _context.Slots.Add(slot); + + _context.SaveChanges(); + + return Ok(world); + } + [HttpPost("{wId}/reset")] [CheckForWorld] [CheckRealmOwner] @@ -522,6 +852,22 @@ namespace Minecraft_Realms_Emulator.Modes.Realms.Controllers { var world = _context.Worlds.Find(wId); + if (world.ParentWorld == null) + { + var snapshotWorld = _context.Worlds.FirstOrDefault(w => w.ParentWorld.Id == wId); + + if (snapshotWorld != null) + { + new DockerHelper(snapshotWorld).DeleteServer(); + _context.Worlds.Remove(snapshotWorld); + } + } + else + { + new DockerHelper(world.ParentWorld).DeleteServer(); + _context.Worlds.Remove(world.ParentWorld); + } + new DockerHelper(world).DeleteServer(); _context.Worlds.Remove(world); diff --git a/Minecraft-Realms-Emulator/Responses/WorldResponse.cs b/Minecraft-Realms-Emulator/Responses/WorldResponse.cs index d839c15..26bf20f 100644 --- a/Minecraft-Realms-Emulator/Responses/WorldResponse.cs +++ b/Minecraft-Realms-Emulator/Responses/WorldResponse.cs @@ -11,5 +11,7 @@ namespace Minecraft_Realms_Emulator.Entities public string Compatibility { get; set; } = null!; public List Slots { get; set; } = null!; public string ActiveVersion { get; set; } = null!; + public int? ParentWorldId { get; set; } + public string? ParentWorldName { get; set; } } } \ No newline at end of file