From 83b2f0543d219bd9cefdcdf7c9fecf5c4937455d Mon Sep 17 00:00:00 2001 From: CyberL1 Date: Sat, 25 May 2024 16:08:33 +0200 Subject: [PATCH] feat: realms mode --- .../Enums/WorkModeEnum.cs | 3 +- .../Minecraft-Realms-Emulator.csproj | 20 +- .../Modes/Realms/InvitesController.cs | 180 +++++++ .../Modes/Realms/McoController.cs | 48 ++ .../Modes/Realms/OpsController.cs | 78 +++ .../Modes/Realms/SubscriptionsController.cs | 38 ++ .../Modes/Realms/TrialController.cs | 30 ++ .../Modes/Realms/WorldsController.cs | 489 ++++++++++++++++++ Minecraft-Realms-Emulator/Program.cs | 27 + .../Resources/files/template/Dockerfile | 15 + 10 files changed, 919 insertions(+), 9 deletions(-) create mode 100644 Minecraft-Realms-Emulator/Modes/Realms/InvitesController.cs create mode 100644 Minecraft-Realms-Emulator/Modes/Realms/McoController.cs create mode 100644 Minecraft-Realms-Emulator/Modes/Realms/OpsController.cs create mode 100644 Minecraft-Realms-Emulator/Modes/Realms/SubscriptionsController.cs create mode 100644 Minecraft-Realms-Emulator/Modes/Realms/TrialController.cs create mode 100644 Minecraft-Realms-Emulator/Modes/Realms/WorldsController.cs create mode 100644 Minecraft-Realms-Emulator/Resources/files/template/Dockerfile diff --git a/Minecraft-Realms-Emulator/Enums/WorkModeEnum.cs b/Minecraft-Realms-Emulator/Enums/WorkModeEnum.cs index 65e6386..0a174ac 100644 --- a/Minecraft-Realms-Emulator/Enums/WorkModeEnum.cs +++ b/Minecraft-Realms-Emulator/Enums/WorkModeEnum.cs @@ -2,6 +2,7 @@ { public enum WorkModeEnum { - EXTERNAL + EXTERNAL, + REALMS } } diff --git a/Minecraft-Realms-Emulator/Minecraft-Realms-Emulator.csproj b/Minecraft-Realms-Emulator/Minecraft-Realms-Emulator.csproj index 091c883..523a8cb 100644 --- a/Minecraft-Realms-Emulator/Minecraft-Realms-Emulator.csproj +++ b/Minecraft-Realms-Emulator/Minecraft-Realms-Emulator.csproj @@ -1,12 +1,16 @@ - + - - net8.0 - enable - enable - true - Minecraft_Realms_Emulator - + + net8.0 + enable + enable + true + Minecraft_Realms_Emulator + + + + + diff --git a/Minecraft-Realms-Emulator/Modes/Realms/InvitesController.cs b/Minecraft-Realms-Emulator/Modes/Realms/InvitesController.cs new file mode 100644 index 0000000..ef2b21d --- /dev/null +++ b/Minecraft-Realms-Emulator/Modes/Realms/InvitesController.cs @@ -0,0 +1,180 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Minecraft_Realms_Emulator.Attributes; +using Minecraft_Realms_Emulator.Data; +using Minecraft_Realms_Emulator.Entities; +using Minecraft_Realms_Emulator.Requests; +using Minecraft_Realms_Emulator.Responses; + +namespace Minecraft_Realms_Emulator.Modes.Realms +{ + [Route("modes/realms/[controller]")] + [ApiController] + [RequireMinecraftCookie] + public class InvitesController : ControllerBase + { + private readonly DataContext _context; + + public InvitesController(DataContext context) + { + _context = context; + } + + [HttpGet("pending")] + public async Task> GetInvites() + { + string cookie = Request.Headers.Cookie; + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + + var invites = await _context.Invites.Where(i => i.RecipeintUUID == playerUUID).Include(i => i.World).ToListAsync(); + + List invitesList = []; + + foreach (var invite in invites) + { + InviteResponse inv = new() + { + InvitationId = invite.InvitationId, + WorldName = invite.World.Name, + WorldOwnerName = invite.World.Owner, + WorldOwnerUuid = invite.World.OwnerUUID, + Date = ((DateTimeOffset) invite.Date).ToUnixTimeMilliseconds(), + }; + + invitesList.Add(inv); + } + + InviteList inviteListRespone = new() + { + Invites = invitesList + }; + + return Ok(inviteListRespone); + } + [HttpPut("accept/{id}")] + public ActionResult AcceptInvite(string id) + { + string cookie = Request.Headers.Cookie; + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + + var invite = _context.Invites.Include(i => i.World).FirstOrDefault(i => i.InvitationId == id); + + if (invite == null) return NotFound("Invite not found"); + + var player = _context.Players.Where(p => p.World.Id == invite.World.Id).FirstOrDefault(p => p.Uuid == playerUUID); + + player.Accepted = true; + + _context.Invites.Remove(invite); + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpPut("reject/{id}")] + public ActionResult RejectInvite(string id) + { + var invite = _context.Invites.Include(i => i.World).FirstOrDefault(i => i.InvitationId == id); + + if (invite == null) return NotFound("Invite not found"); + + _context.Invites.Remove(invite); + + string cookie = Request.Headers.Cookie; + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + + var player = _context.Players.Where(p => p.World.Id == invite.World.Id).FirstOrDefault(p => p.Uuid == playerUUID); + + _context.Players.Remove(player); + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpPost("{wId}")] + [CheckRealmOwner] + public async Task> InvitePlayer(int wId, PlayerRequest body) + { + string cookie = Request.Headers.Cookie; + string playerName = cookie.Split(";")[1].Split("=")[1]; + + if (body.Name == playerName) return Forbid("You cannot invite yourself"); + + var world = await _context.Worlds.Include(w => w.Players).FirstOrDefaultAsync(w => w.Id == wId); + + if (world == null) return NotFound("World not found"); + + // Get player UUID + var playerInfo = await new HttpClient().GetFromJsonAsync($"https://api.mojang.com/users/profiles/minecraft/{body.Name}"); + + var playerInDB = await _context.Players.Where(p => p.World.Id == wId).FirstOrDefaultAsync(p => p.Uuid == playerInfo.Id); + + if (playerInDB?.Uuid == playerInfo.Id) return BadRequest("Player already invited"); + + Player player = new() + { + Name = body.Name, + Uuid = playerInfo.Id, + World = world + }; + + _context.Players.Add(player); + + Invite invite = new() + { + InvitationId = Guid.NewGuid().ToString(), + World = world, + RecipeintUUID = playerInfo.Id, + Date = DateTime.UtcNow, + }; + + _context.Invites.Add(invite); + + _context.SaveChanges(); + + return Ok(world); + } + + [HttpDelete("{wId}/invite/{uuid}")] + [CheckRealmOwner] + public async Task> DeleteInvite(int wId, string uuid) + { + var world = await _context.Worlds.FirstOrDefaultAsync(w => w.Id == wId); + + if (world == null) return NotFound("World not found"); + + var player = _context.Players.Where(p => p.World.Id == wId).FirstOrDefault(p => p.Uuid == uuid); + + _context.Players.Remove(player); + + var invite = await _context.Invites.FirstOrDefaultAsync(i => i.RecipeintUUID == uuid); + + if (invite != null) _context.Invites.Remove(invite); + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpDelete("{wId}")] + public async Task> LeaveWorld(int wId) + { + string cookie = Request.Headers.Cookie; + string playerUUID = cookie.Split(";")[0].Split(":")[2]; + + var world = await _context.Worlds.FirstOrDefaultAsync(w => w.Id == wId); + + if (world == null) return NotFound("World not found"); + + var player = _context.Players.Where(p => p.World.Id == wId).FirstOrDefault(p => p.Uuid == playerUUID); + + _context.Players.Remove(player); + + _context.SaveChanges(); + + return Ok(true); + } + } +} diff --git a/Minecraft-Realms-Emulator/Modes/Realms/McoController.cs b/Minecraft-Realms-Emulator/Modes/Realms/McoController.cs new file mode 100644 index 0000000..3bfda6a --- /dev/null +++ b/Minecraft-Realms-Emulator/Modes/Realms/McoController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using Minecraft_Realms_Emulator.Attributes; +using Minecraft_Realms_Emulator.Data; +using Minecraft_Realms_Emulator.Enums; +using Minecraft_Realms_Emulator.Helpers; +using Minecraft_Realms_Emulator.Responses; + +namespace Minecraft_Realms_Emulator.Modes.Realms +{ + [Route("modes/realms/[controller]")] + [ApiController] + [RequireMinecraftCookie] + public class McoController : ControllerBase + { + private readonly DataContext _context; + + public McoController(DataContext context) + { + _context = context; + } + + [HttpGet("available")] + public ActionResult GetAvailable() + { + return Ok(true); + } + + [HttpGet("client/compatible")] + public ActionResult GetCompatible() + { + return Ok(nameof(VersionCompatibilityEnum.COMPATIBLE)); + } + + [HttpGet("v1/news")] + public ActionResult GetNews() + { + var config = new ConfigHelper(_context); + var newsLink = config.GetSetting(nameof(SettingsEnum.NewsLink)); + + var news = new NewsResponse + { + NewsLink = newsLink.Value + }; + + return Ok(news); + } + } +} \ No newline at end of file diff --git a/Minecraft-Realms-Emulator/Modes/Realms/OpsController.cs b/Minecraft-Realms-Emulator/Modes/Realms/OpsController.cs new file mode 100644 index 0000000..38c5628 --- /dev/null +++ b/Minecraft-Realms-Emulator/Modes/Realms/OpsController.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; +using Minecraft_Realms_Emulator.Attributes; +using Minecraft_Realms_Emulator.Data; +using Minecraft_Realms_Emulator.Responses; + +namespace Minecraft_Realms_Emulator.Modes.Realms +{ + [Route("modes/realms/[controller]")] + [ApiController] + [RequireMinecraftCookie] + public class OpsController : ControllerBase + { + private readonly DataContext _context; + + public OpsController(DataContext context) + { + _context = context; + } + + [HttpPost("{wId}/{uuid}")] + [CheckRealmOwner] + public ActionResult OpPlayer(int wId, string uuid) + { + var ops = _context.Players.Where(p => p.World.Id == wId && p.Operator == true).ToList(); + var player = _context.Players.Where(p => p.World.Id == wId).FirstOrDefault(p => p.Uuid == uuid); + + List opNames = []; + + foreach (var op in ops) + { + opNames.Add(op.Name); + } + + player.Permission = "OPERATOR"; + player.Operator = true; + + _context.SaveChanges(); + + opNames.Add(player.Name); + + var opsResponse = new OpsResponse + { + Ops = opNames + }; + + return Ok(opsResponse); + } + + [HttpDelete("{wId}/{uuid}")] + [CheckRealmOwner] + public ActionResult DeopPlayer(int wId, string uuid) + { + var ops = _context.Players.Where(p => p.World.Id == wId && p.Operator == true).ToList(); + var player = _context.Players.Where(p => p.World.Id == wId).FirstOrDefault(p => p.Uuid == uuid); + + List opNames = []; + + foreach (var op in ops) + { + opNames.Add(op.Name); + } + + player.Permission = "MEMBER"; + player.Operator = false; + + _context.SaveChanges(); + + opNames.Remove(player.Name); + + var opsResponse = new OpsResponse + { + Ops = opNames + }; + + return Ok(opsResponse); + } + } +} diff --git a/Minecraft-Realms-Emulator/Modes/Realms/SubscriptionsController.cs b/Minecraft-Realms-Emulator/Modes/Realms/SubscriptionsController.cs new file mode 100644 index 0000000..1ae2f7f --- /dev/null +++ b/Minecraft-Realms-Emulator/Modes/Realms/SubscriptionsController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Minecraft_Realms_Emulator.Attributes; +using Minecraft_Realms_Emulator.Data; +using Minecraft_Realms_Emulator.Responses; + +namespace Minecraft_Realms_Emulator.Modes.Realms +{ + [Route("modes/realms/[controller]")] + [ApiController] + [RequireMinecraftCookie] + public class SubscriptionsController : ControllerBase + { + private readonly DataContext _context; + + public SubscriptionsController(DataContext context) + { + _context = context; + } + [HttpGet("{wId}")] + [CheckRealmOwner] + public async Task> Get(int wId) + { + var world = await _context.Worlds.Include(w => w.Subscription).FirstOrDefaultAsync(w => w.Id == wId); + + if (world?.Subscription == null) return NotFound("Subscription not found"); + + var sub = new SubscriptionResponse + { + StartDate = ((DateTimeOffset)world.Subscription.StartDate).ToUnixTimeMilliseconds(), + DaysLeft = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days, + SubscriptionType = world.Subscription.SubscriptionType + }; + + return Ok(sub); + } + } +} diff --git a/Minecraft-Realms-Emulator/Modes/Realms/TrialController.cs b/Minecraft-Realms-Emulator/Modes/Realms/TrialController.cs new file mode 100644 index 0000000..2a108f3 --- /dev/null +++ b/Minecraft-Realms-Emulator/Modes/Realms/TrialController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Minecraft_Realms_Emulator.Attributes; +using Minecraft_Realms_Emulator.Data; +using Minecraft_Realms_Emulator.Enums; +using Minecraft_Realms_Emulator.Helpers; + +namespace Minecraft_Realms_Emulator.Modes.Realms +{ + [Route("modes/realms/[controller]")] + [ApiController] + [RequireMinecraftCookie] + public class TrialController : ControllerBase + { + private readonly DataContext _context; + + public TrialController(DataContext context) + { + _context = context; + } + + [HttpGet] + public ActionResult GetTrial() + { + var config = new ConfigHelper(_context); + var trialMode = config.GetSetting(nameof(SettingsEnum.TrialMode)); + + return Ok(trialMode.Value); + } + } +} diff --git a/Minecraft-Realms-Emulator/Modes/Realms/WorldsController.cs b/Minecraft-Realms-Emulator/Modes/Realms/WorldsController.cs new file mode 100644 index 0000000..e6b9579 --- /dev/null +++ b/Minecraft-Realms-Emulator/Modes/Realms/WorldsController.cs @@ -0,0 +1,489 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Minecraft_Realms_Emulator.Attributes; +using Minecraft_Realms_Emulator.Data; +using Minecraft_Realms_Emulator.Entities; +using Minecraft_Realms_Emulator.Enums; +using Minecraft_Realms_Emulator.Helpers; +using Minecraft_Realms_Emulator.Requests; +using Minecraft_Realms_Emulator.Responses; +using Newtonsoft.Json; +using Semver; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; + +namespace Minecraft_Realms_Emulator.Modes.Realms +{ + [Route("modes/realms/[controller]")] + [ApiController] + [RequireMinecraftCookie] + public class WorldsController : ControllerBase + { + private readonly DataContext _context; + + public WorldsController(DataContext context) + { + _context = context; + } + + [HttpGet] + public async Task> GetWorlds() + { + 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).Include(w => w.Subscription).Include(w => w.Slots).ToListAsync(); + var memberWorlds = await _context.Players.Where(p => p.Uuid == playerUUID && p.Accepted).Include(p => p.World.Subscription).Include(p => p.World.Slots).Select(p => p.World).ToListAsync(); + + List allWorlds = []; + + if (ownedWorlds.ToArray().Length == 0) + { + var world = new World + { + Owner = playerName, + OwnerUUID = playerUUID, + Name = null, + Motd = null, + State = nameof(StateEnum.UNINITIALIZED), + WorldType = nameof(WorldTypeEnum.NORMAL), + MaxPlayers = 10, + MinigameId = null, + MinigameName = null, + MinigameImage = null, + ActiveSlot = 1, + Member = false + }; + + 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 = SemVersion.Parse(gameVersion, SemVersionStyles.OptionalPatch).ComparePrecedenceTo(SemVersion.Parse(activeSlot?.Version ?? gameVersion, SemVersionStyles.Any)); + 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; + } + + allWorlds.Add(response); + } + + foreach (var world in memberWorlds) + { + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + int versionsCompared = SemVersion.Parse(gameVersion, SemVersionStyles.OptionalPatch).ComparePrecedenceTo(SemVersion.Parse(activeSlot.Version, SemVersionStyles.OptionalPatch)); + 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}")] + [CheckRealmOwner] + public async Task> GetWorldById(int wId) + { + 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); + + if (world?.Subscription == null) return NotFound("World not found"); + + Slot activeSlot = world.Slots.Find(s => s.SlotId == world.ActiveSlot); + + List slots = []; + + foreach (var slot in world.Slots) + { + int versionsCompared = SemVersion.Parse(gameVersion, SemVersionStyles.OptionalPatch).ComparePrecedenceTo(SemVersion.Parse(slot.Version, SemVersionStyles.OptionalPatch)); + string compatibility = versionsCompared == 0 ? nameof(CompatibilityEnum.COMPATIBLE) : versionsCompared < 0 ? nameof(CompatibilityEnum.NEEDS_DOWNGRADE) : nameof(CompatibilityEnum.NEEDS_UPGRADE); + + slots.Add(new SlotResponse() + { + SlotId = slot.SlotId, + Options = JsonConvert.SerializeObject(new + { + slotName = slot.SlotName, + gameMode = slot.GameMode, + difficulty = slot.Difficulty, + spawnProtection = slot.SpawnProtection, + forceGameMode = slot.ForceGameMode, + pvp = slot.Pvp, + spawnAnimals = slot.SpawnAnimals, + spawnMonsters = slot.SpawnMonsters, + spawnNPCs = slot.SpawnNPCs, + commandBlocks = slot.CommandBlocks, + version = slot.Version, + compatibility + }) + }); + } + + var activeSlotOptions = JsonConvert.DeserializeObject(slots.Find(s => s.SlotId == activeSlot.SlotId).Options); + + 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, + Slots = slots, + Member = world.Member, + Players = world.Players, + DaysLeft = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days, + Expired = ((DateTimeOffset)world.Subscription.StartDate.AddDays(30) - DateTime.Today).Days < 0, + ExpiredTrial = false, + ActiveVersion = activeSlotOptions.Version, + Compatibility = activeSlotOptions.Compatibility + }; + + return response; + } + + [HttpPost("{wId}/initialize")] + [CheckRealmOwner] + public async Task> Initialize(int wId, WorldCreateRequest body) + { + 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 == null) return NotFound("World not found"); + if (world.State != nameof(StateEnum.UNINITIALIZED)) return NotFound("World already initialized"); + + var subscription = new Subscription + { + StartDate = DateTime.UtcNow, + SubscriptionType = nameof(SubscriptionTypeEnum.NORMAL) + }; + + world.Name = body.Name; + world.Motd = body.Description; + world.State = nameof(StateEnum.OPEN); + world.Subscription = subscription; + + 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 + }; + + // Run docker container + ProcessStartInfo serverProcessInfo = new(); + + serverProcessInfo.FileName = "docker"; + serverProcessInfo.Arguments = $"run -d --name realm-server-{world.Id} -p {port}:25565 realm-server"; + + Process serverProcess = new(); + serverProcess.StartInfo = serverProcessInfo; + serverProcess.Start(); + + _context.Worlds.Update(world); + + _context.Subscriptions.Add(subscription); + _context.Connections.Add(connection); + _context.Slots.Add(slot); + + _context.SaveChanges(); + + return Ok(world); + } + + [HttpPost("{wId}/reset")] + [CheckRealmOwner] + public ActionResult Reset(int wId) + { + Console.WriteLine($"Resetting world {wId}"); + return Ok(true); + } + + [HttpPut("{wId}/open")] + [CheckRealmOwner] + public async Task> Open(int wId) + { + var worlds = await _context.Worlds.ToListAsync(); + + var world = worlds.Find(w => w.Id == wId); + + if (world == null) return NotFound("World not found"); + + // Start the server + ProcessStartInfo serverProcessInfo = new(); + + serverProcessInfo.FileName = "docker"; + serverProcessInfo.Arguments = $"container start realm-server-{world.Id}"; + + Process serverProcess = new(); + serverProcess.StartInfo = serverProcessInfo; + serverProcess.Start(); + + world.State = nameof(StateEnum.OPEN); + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpPut("{wId}/close")] + [CheckRealmOwner] + public async Task> Close(int wId) + { + var worlds = await _context.Worlds.ToListAsync(); + + var world = worlds.FirstOrDefault(w => w.Id == wId); + + if (world == null) return NotFound("World not found"); + + // Stop the server + ProcessStartInfo serverProcessInfo = new(); + + serverProcessInfo.FileName = "docker"; + serverProcessInfo.Arguments = $"container stop realm-server-{world.Id}"; + + Process serverProcess = new(); + serverProcess.StartInfo = serverProcessInfo; + serverProcess.Start(); + + world.State = nameof(StateEnum.CLOSED); + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpPost("{wId}")] + [CheckRealmOwner] + public async Task> UpdateWorld(int wId, WorldCreateRequest body) + { + var worlds = await _context.Worlds.ToListAsync(); + + var world = worlds.Find(w => w.Id == wId); + + if (world == null) return NotFound("World not found"); + + world.Name = body.Name; + world.Motd = body.Description; + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpPost("{wId}/slot/{sId}")] + [CheckRealmOwner] + public async Task> UpdateSlotAsync(int wId, int sId, SlotOptionsRequest body) + { + var slots = await _context.Slots.Where(s => s.World.Id == wId).ToListAsync(); + var slot = slots.Find(s => s.SlotId == sId); + + slot.SlotName = body.SlotName; + slot.GameMode = body.GameMode; + slot.Difficulty = body.Difficulty; + slot.SpawnProtection = body.SpawnProtection; + slot.ForceGameMode = body.ForceGameMode; + slot.Pvp = body.Pvp; + slot.SpawnAnimals = body.SpawnAnimals; + slot.SpawnMonsters = body.SpawnMonsters; + slot.SpawnNPCs = body.SpawnNPCs; + slot.CommandBlocks = body.CommandBlocks; + + _context.SaveChanges(); + + return Ok(true); + } + + [HttpPut("{wId}/slot/{sId}")] + [CheckRealmOwner] + public ActionResult SwitchSlot(int wId, int sId) + { + var world = _context.Worlds.Find(wId); + + if (world == null) return NotFound("World not found"); + + var slot = _context.Slots.Where(s => s.World.Id == wId).Where(s => s.SlotId == sId).Any(); + + if (!slot) + { + string cookie = Request.Headers.Cookie; + string gameVersion = cookie.Split(";")[2].Split("=")[1]; + + _context.Slots.Add(new() + { + World = world, + SlotId = sId, + SlotName = "", + Version = gameVersion, + GameMode = 0, + Difficulty = 2, + SpawnProtection = 0, + ForceGameMode = false, + Pvp = true, + SpawnAnimals = true, + SpawnMonsters = true, + SpawnNPCs = true, + CommandBlocks = false + }); + + _context.SaveChanges(); + } + + world.ActiveSlot = sId; + _context.SaveChanges(); + + return Ok(true); + } + + [HttpGet("{wId}/backups")] + [CheckRealmOwner] + public async Task> GetBackups(int wId) + { + var backups = await _context.Backups.Where(b => b.World.Id == wId).ToListAsync(); + + BackupsResponse worldBackups = new() + { + Backups = backups + }; + + return Ok(worldBackups); + } + + [HttpGet("v1/{wId}/join/pc")] + public ActionResult Join(int wId) + { + var connection = _context.Connections.FirstOrDefault(x => x.World.Id == wId); + + return Ok(connection); + } + + [HttpDelete("{wId}")] + [CheckRealmOwner] + public ActionResult DeleteRealm(int wId) + { + var world = _context.Worlds.Find(wId); + + if (world == null) return NotFound("World not found"); + + // Remove docker container + ProcessStartInfo serverProcessInfo = new(); + + serverProcessInfo.FileName = "docker"; + serverProcessInfo.Arguments = $"container rm realm-server-{world.Id}"; + + Process serverProcess = new(); + serverProcess.StartInfo = serverProcessInfo; + serverProcess.Start(); + + _context.Worlds.Remove(world); + _context.SaveChanges(); + + return Ok(true); + } + } +} diff --git a/Minecraft-Realms-Emulator/Program.cs b/Minecraft-Realms-Emulator/Program.cs index 265462b..f3dccdf 100644 --- a/Minecraft-Realms-Emulator/Program.cs +++ b/Minecraft-Realms-Emulator/Program.cs @@ -5,6 +5,7 @@ using Minecraft_Realms_Emulator.Enums; using Minecraft_Realms_Emulator.Helpers; using Minecraft_Realms_Emulator.Middlewares; using Npgsql; +using System.Reflection; var builder = WebApplication.CreateBuilder(args); DotNetEnv.Env.Load(); @@ -63,6 +64,32 @@ if (!Enum.IsDefined(typeof(WorkModeEnum), mode.Value)) Environment.Exit(1); } +if (mode.Value == nameof(WorkModeEnum.REALMS)) +{ + var resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames(); + + foreach (var resourceName in resourceNames) + { + var path = $"{AppDomain.CurrentDomain.BaseDirectory}{resourceName.Replace("Minecraft_Realms_Emulator.Resources.", "").Replace(".", "/")}"; + + var directory = Path.GetDirectoryName(path); + var name = Path.GetFileName(path); + + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + if (!File.Exists(path)) + { + using var file = new FileStream(path, FileMode.Create); + stream.CopyTo(file); + } + } +} + var rewriteOptions = new RewriteOptions().AddRewrite(@"^(?!.*configuration)(.*)$", $"modes/{mode.Value}/$1", true); app.UseRewriter(rewriteOptions); diff --git a/Minecraft-Realms-Emulator/Resources/files/template/Dockerfile b/Minecraft-Realms-Emulator/Resources/files/template/Dockerfile new file mode 100644 index 0000000..e8bb672 --- /dev/null +++ b/Minecraft-Realms-Emulator/Resources/files/template/Dockerfile @@ -0,0 +1,15 @@ +FROM eclipse-temurin:21 + +WORKDIR /server +COPY . . + +RUN mkdir mc +WORKDIR mc + +RUN wget -O server.jar https://piston-data.mojang.com/v1/objects/145ff0858209bcfc164859ba735d4199aafa1eea/server.jar + +EXPOSE 25565 + +RUN java -jar server.jar +RUN echo eula=true > eula.txt +CMD ["java", "-jar", "server.jar"] \ No newline at end of file