From 67aafeb9e9f8191ba81b166fb16a856ecc96161d Mon Sep 17 00:00:00 2001 From: "Senzaiken\\Uri" Date: Tue, 16 Jun 2026 21:51:27 -0400 Subject: [PATCH] Add multi-world editing with server-controlled facets --- CentrED/Application.cs | 4 +- CentrED/CentrEDGame.cs | 44 +++- CentrED/Config.cs | 30 ++- CentrED/Map/Facet.cs | 138 ++++++++++ CentrED/Map/FacetController.cs | 277 ++++++++++++++++++++ CentrED/Map/FacetServerHost.cs | 111 ++++++++ CentrED/Map/LandObject.cs | 25 +- CentrED/Map/MapManager.cs | 271 +++++++++++-------- CentrED/Map/RadarMap.cs | 66 +++-- CentrED/Map/StaticObject.cs | 13 +- CentrED/Map/StaticsManager.cs | 6 +- CentrED/Map/TileObject.cs | 1 + CentrED/Map/World.cs | 96 +++++++ CentrED/UI/UIManager.cs | 178 ++++++++++++- CentrED/UI/Windows/MinimapWindow.cs | 34 ++- CentrED/UI/Windows/OptionsWindow.cs | 161 ++++++++++++ Client/CentrEDClient.cs | 18 +- Client/ConnectionHandling.cs | 17 ++ Client/Packets.cs | 17 ++ Server/AdminHandling.cs | 3 +- Server/CEDServer.cs | 79 ++++-- Server/Config/ConfigRoot.cs | 28 +- Server/Config/Map.cs | 10 +- Server/ConnectionHandling.cs | 22 +- Server/Map/ServerLandscape.cs | 38 ++- Server/Map/ServerLandscapePacketHandlers.cs | 22 +- Server/Packets.cs | 37 ++- Server/ServerNetState.cs | 6 + 28 files changed, 1504 insertions(+), 248 deletions(-) create mode 100644 CentrED/Map/Facet.cs create mode 100644 CentrED/Map/FacetController.cs create mode 100644 CentrED/Map/FacetServerHost.cs create mode 100644 CentrED/Map/World.cs diff --git a/CentrED/Application.cs b/CentrED/Application.cs index 0ae1d949..12e2d191 100644 --- a/CentrED/Application.cs +++ b/CentrED/Application.cs @@ -11,7 +11,9 @@ public class Application public static CentrEDGame CEDGame { get; private set; } = null!; public static CEDServer? CEDServer; - public static readonly CentrEDClient CEDClient = new(); + private static readonly CentrEDClient _bootstrapClient = new(); + public static CentrEDClient BootstrapClient => _bootstrapClient; + public static CentrEDClient CEDClient => CEDGame?.Worlds?.ActiveClient ?? _bootstrapClient; public static readonly Metrics Metrics = new(); [STAThread] diff --git a/CentrED/CentrEDGame.cs b/CentrED/CentrEDGame.cs index 986abadd..8ef85198 100644 --- a/CentrED/CentrEDGame.cs +++ b/CentrED/CentrEDGame.cs @@ -15,8 +15,10 @@ public class CentrEDGame : Game public readonly GraphicsDeviceManager _gdm; private Keymap _keymap; - public MapManager MapManager; + public WorldManager Worlds = new(); + public MapManager MapManager => Worlds.Active!.Map; public UIManager UIManager; + public FacetController FacetController; public bool Closing { get; set; } public CentrEDGame() @@ -54,8 +56,11 @@ protected override void Initialize() //UIManager have to exist before MapManager, since Tools can be dependent on Windows _keymap = new Keymap(); UIManager = new UIManager(_gdm.GraphicsDevice, Window, _keymap); - MapManager = new MapManager(_gdm.GraphicsDevice, Window, _keymap); - RadarMap.Initialize(_gdm.GraphicsDevice); + Worlds.Initialize(_gdm.GraphicsDevice, Window, _keymap); + Worlds.CreateWorld(Application.BootstrapClient); + FacetController = new FacetController(); + FacetController.Refresh(); + FacetController.SyncWorlds(); base.Initialize(); } @@ -69,6 +74,7 @@ protected override void BeginRun() protected override void UnloadContent() { CEDClient.Disconnect(); + FacetController?.Shutdown(); } protected override void Update(GameTime gameTime) @@ -76,11 +82,25 @@ protected override void Update(GameTime gameTime) try { _keymap.Update(Keyboard.GetState()); + FacetController.ProcessSync(); + foreach (var world in Worlds.Worlds) + { + if (world.Map.WindowVisible) + FacetController.EnsureLoaded(world); + } Metrics.Start("UpdateClient"); - if(CEDClient.Running) - CEDClient.Update(); + foreach (var world in Worlds.Worlds) + { + if (world.Client.Running && !FacetController.IsBusy(world)) + world.Client.Update(); + } Metrics.Stop("UpdateClient"); - MapManager.Update(gameTime, IsActive, !UIManager.CapturingMouse, !UIManager.CapturingKeyboard); + foreach (var world in Worlds.Worlds) + { + if (FacetController.IsBusy(world) || !world.Map.WindowVisible) + continue; + world.Map.Update(gameTime, IsActive, world.Map.ViewHovered, world.Map.ViewFocused); + } Config.AutoSave(); } catch(Exception e) @@ -119,11 +139,19 @@ protected override void Draw(GameTime gameTime) try { Metrics.Start("Draw"); - MapManager.Draw(); + foreach (var world in Worlds.Worlds) + { + if (FacetController.IsBusy(world) || !world.Map.WindowVisible) + continue; + world.Map.Draw(); + } + GraphicsDevice.SetRenderTarget(null); + GraphicsDevice.Clear(Color.Black); UIManager.Draw(); Present(); UIManager.DrawExtraWindows(); - MapManager.AfterDraw(); + foreach (var world in Worlds.Worlds) + world.Map.AfterDraw(); Metrics.Stop("Draw"); } catch (Exception e) diff --git a/CentrED/Config.cs b/CentrED/Config.cs index 015befe8..d3a5d6d3 100644 --- a/CentrED/Config.cs +++ b/CentrED/Config.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +using System.Security.Cryptography; +using System.Text.Json; +using System.Text.Json.Serialization; using CentrED.IO.Models; using Microsoft.Xna.Framework.Input; @@ -16,10 +18,36 @@ public class ImageOverlaySettings public float Screen = 0.0f; } +public class FacetOverride +{ + public int Index; + public string Name = ""; + public int Width; + public int Height; +} + +public class FacetSettings +{ + public string MapsFolder = ""; + public int BasePort = 2598; + public List Overrides = new(); + + [JsonIgnore] public string AdminUsername => "admin"; + [JsonIgnore] public string AdminPassword { get; } = GenerateSecret(); + + private static string GenerateSecret() + { + Span bytes = stackalloc byte[24]; + RandomNumberGenerator.Fill(bytes); + return Convert.ToHexString(bytes); + } +} + public class ConfigRoot { public string ActiveProfile = ""; public string ServerConfigPath = "cedserver.xml"; + public FacetSettings Facets = new(); public bool PreferTexMaps; public bool ObjectBrightHighlight; public bool LegacyMouseScroll; diff --git a/CentrED/Map/Facet.cs b/CentrED/Map/Facet.cs new file mode 100644 index 00000000..558647f7 --- /dev/null +++ b/CentrED/Map/Facet.cs @@ -0,0 +1,138 @@ +using System.Text.RegularExpressions; + +namespace CentrED.Map; + +public class Facet +{ + public int Index; + public string Name = ""; + public string MapPath = ""; + public string StaIdxPath = ""; + public string StaticsPath = ""; + public ushort Width; + public ushort Height; + public int Port; + public bool IsUop; + public bool DimensionsKnown; + + public bool HasStatics => StaIdxPath.Length > 0 && StaticsPath.Length > 0; + public override string ToString() => $"{Name} ({Width}x{Height} @ :{Port})"; +} + +public partial class FacetManager +{ + public List Facets { get; } = new(); + public string ScannedFolder { get; private set; } = ""; + + private const int BytesPerLandBlock = 196; + + private static readonly string[] FacetNames = + { "Felucca", "Trammel", "Ilshenar", "Malas", "Tokuno", "TerMur" }; + + private static readonly (ushort W, ushort H)[] DimensionCandidates = + { + (768, 512), (896, 512), (288, 200), (320, 256), (160, 512), (320, 512), (181, 181), (128, 128) + }; + + private static readonly (ushort W, ushort H)[] ConventionalDimensions = + { + (896, 512), (896, 512), (288, 200), (320, 256), (181, 181), (160, 512) + }; + + [GeneratedRegex(@"^map(\d+)(LegacyMUL)?\.(mul|uop)$", RegexOptions.IgnoreCase)] + private static partial Regex MapFileRegex(); + + public void Scan(string folder, FacetSettings settings) + { + Facets.Clear(); + ScannedFolder = folder; + if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) + return; + + foreach (var path in Directory.EnumerateFiles(folder)) + { + var match = MapFileRegex().Match(Path.GetFileName(path)); + if (!match.Success) + continue; + if (!int.TryParse(match.Groups[1].Value, out var index)) + continue; + + var isUop = match.Groups[3].Value.Equals("uop", StringComparison.OrdinalIgnoreCase); + var facet = new Facet + { + Index = index, + Name = index < FacetNames.Length ? FacetNames[index] : $"Map {index}", + MapPath = path, + IsUop = isUop, + Port = settings.BasePort, + }; + FindStatics(folder, index, facet); + ResolveDimensions(facet, settings); + Facets.Add(facet); + } + Facets.Sort((a, b) => a.Index.CompareTo(b.Index)); + } + + private static void FindStatics(string folder, int index, Facet facet) + { + foreach (var candidate in new[] { $"staidx{index}.mul", $"staidx{index}LegacyMUL.uop" }) + { + var p = Path.Combine(folder, candidate); + if (File.Exists(p)) { facet.StaIdxPath = p; break; } + } + foreach (var candidate in new[] { $"statics{index}.mul", $"statics{index}LegacyMUL.uop" }) + { + var p = Path.Combine(folder, candidate); + if (File.Exists(p)) { facet.StaticsPath = p; break; } + } + } + + private static void ResolveDimensions(Facet facet, FacetSettings settings) + { + var ovr = settings.Overrides.Find(o => o.Index == facet.Index); + if (ovr != null && !string.IsNullOrWhiteSpace(ovr.Name)) + facet.Name = ovr.Name; + if (ovr != null && ovr.Width > 0 && ovr.Height > 0) + { + facet.Width = (ushort)ovr.Width; + facet.Height = (ushort)ovr.Height; + facet.DimensionsKnown = true; + return; + } + + var (w, h, known) = GuessDimensions(facet); + facet.Width = w; + facet.Height = h; + facet.DimensionsKnown = known; + } + + private static (ushort W, ushort H, bool Known) GuessDimensions(Facet facet) + { + var conventional = facet.Index < ConventionalDimensions.Length + ? ConventionalDimensions[facet.Index] + : ((ushort)0, (ushort)0); + + if (facet.IsUop) + return (conventional.Item1, conventional.Item2, conventional.Item1 > 0); + + long size; + try { size = new FileInfo(facet.MapPath).Length; } + catch { return (conventional.Item1, conventional.Item2, false); } + + if (size <= 0 || size % BytesPerLandBlock != 0) + return (conventional.Item1, conventional.Item2, false); + + var totalBlocks = size / BytesPerLandBlock; + var matches = DimensionCandidates.Where(c => (long)c.W * c.H == totalBlocks).ToList(); + if (matches.Count == 1) + return (matches[0].W, matches[0].H, true); + if (matches.Count > 1) + { + var pick = matches.FirstOrDefault(m => m.W == conventional.Item1 && m.H == conventional.Item2); + if (pick.W > 0) + return (pick.W, pick.H, true); + return (matches[0].W, matches[0].H, false); + } + return (conventional.Item1, conventional.Item2, false); + } +} diff --git a/CentrED/Map/FacetController.cs b/CentrED/Map/FacetController.cs new file mode 100644 index 00000000..d2a1803c --- /dev/null +++ b/CentrED/Map/FacetController.cs @@ -0,0 +1,277 @@ +using CentrED.IO; +using static CentrED.Application; +using Point = System.Drawing.Point; + +namespace CentrED.Map; + +public class FacetController +{ + public FacetManager Manager { get; } = new(); + private readonly FacetServerHost _host = new(); + private readonly Dictionary _worldsByFacet = new(); + private readonly HashSet _loadAttempted = new(); + private bool _syncRequested; + + private sealed record RemoteSession(string Host, int Port, string User, string Pass, List Facets); + private RemoteSession? _remoteSession; + public bool InRemoteMode => _remoteSession != null; + public IReadOnlyList RemoteFacets => + _remoteSession?.Facets ?? (IReadOnlyList)Array.Empty(); + + private volatile World? _busyWorld; + public World? BusyWorld { get => _busyWorld; private set => _busyWorld = value; } + + private volatile string _status = ""; + public string Status { get => _status; private set => _status = value; } + + public IReadOnlyList Facets => Manager.Facets; + public bool IsHosted(int index) => _host.IsRunning(index); + public bool IsBusy(World world) => BusyWorld == world; + public bool Switching => BusyWorld != null; + + public void Refresh() => Manager.Scan(Config.Instance.Facets.MapsFolder, Config.Instance.Facets); + + public List ValidFacets() => Facets.Where(f => f.DimensionsKnown && f.HasStatics).ToList(); + + public bool IsOpen(int facetIndex) => + _worldsByFacet.TryGetValue(facetIndex, out var w) && CEDGame.Worlds.Worlds.Contains(w); + + public void RequestSync() => _syncRequested = true; + + public void ProcessSync() + { + if (_syncRequested) + { + _syncRequested = false; + SyncWorlds(); + } + TryAdoptRemoteFacets(); + TryLeaveRemoteFacets(); + } + + private void TryAdoptRemoteFacets() + { + if (_remoteSession != null || Manager.Facets.Count > 0) + return; + var world = CEDGame.Worlds.Active; + if (world == null || !world.Client.Running || world.Client.ServerFacets.Count <= 1) + return; + AdoptRemoteFacets(world); + } + + private void TryLeaveRemoteFacets() + { + if (_remoteSession == null || BusyWorld != null) + return; + var worlds = CEDGame.Worlds; + if (worlds.Worlds.Any(w => w.Client.Running)) + return; + var keep = worlds.Active ?? (worlds.Worlds.Count > 0 ? worlds.Worlds[0] : worlds.CreateWorld(new Client.CentrEDClient())); + foreach (var w in worlds.Worlds.ToList()) + if (w != keep) + CloseWorld(w); + ResetToPlain(keep); + _remoteSession = null; + worlds.RequestFocus(keep); + } + + private void AdoptRemoteFacets(World connected) + { + var worlds = CEDGame.Worlds; + var client = connected.Client; + var facets = client.ServerFacets.ToList(); + _remoteSession = new RemoteSession(client.Hostname, client.Port, client.Username ?? "", client.Password ?? "", facets); + + var primaryIndex = client.Facet; + var primary = facets.Find(f => f.Index == primaryIndex); + connected.FacetIndex = primaryIndex; + connected.Name = string.IsNullOrEmpty(primary.Name) ? $"Facet {primaryIndex}" : primary.Name; + _worldsByFacet[primaryIndex] = connected; + _loadAttempted.Add(connected); + + foreach (var facet in facets) + { + if (facet.Index == primaryIndex || _worldsByFacet.ContainsKey(facet.Index)) + continue; + var world = worlds.FindIdleWorld() ?? worlds.CreateWorld(new Client.CentrEDClient()); + world.FacetIndex = facet.Index; + world.Name = string.IsNullOrEmpty(facet.Name) ? $"Facet {facet.Index}" : facet.Name; + _worldsByFacet[facet.Index] = world; + } + worlds.RequestFocus(connected); + } + + public void SyncWorlds() + { + _remoteSession = null; + var worlds = CEDGame.Worlds; + var facets = Facets.Where(f => f.DimensionsKnown && f.HasStatics).ToList(); + + if (facets.Count == 0) + { + var keep = worlds.Active ?? (worlds.Worlds.Count > 0 ? worlds.Worlds[0] : worlds.CreateWorld(new Client.CentrEDClient())); + foreach (var w in worlds.Worlds.ToList()) + if (w != keep) + CloseWorld(w); + ResetToPlain(keep); + worlds.RequestFocus(keep); + return; + } + + foreach (var facet in facets) + { + if (IsOpen(facet.Index)) + continue; + var world = worlds.FindIdleWorld() ?? worlds.CreateWorld(new Client.CentrEDClient()); + world.Name = facet.Name; + world.FacetIndex = facet.Index; + _worldsByFacet[facet.Index] = world; + } + foreach (var w in worlds.Worlds.ToList()) + { + var isCurrentFacet = w.FacetIndex >= 0 && facets.Exists(f => f.Index == w.FacetIndex); + if (!isCurrentFacet) + CloseWorld(w); + } + if (_worldsByFacet.TryGetValue(facets[0].Index, out var first)) + worlds.RequestFocus(first); + } + + private void ResetToPlain(World world) + { + try + { + if (world.Client.Running) + world.Client.Disconnect(); + } + catch { } + if (world.FacetIndex >= 0) + _worldsByFacet.Remove(world.FacetIndex); + world.FacetIndex = -1; + world.Name = "World"; + _loadAttempted.Remove(world); + } + + public void EnsureLoaded(World world) + { + if (BusyWorld != null || world.FacetIndex < 0 || world.Client.Running) + return; + if (!_loadAttempted.Add(world)) + return; + if (_remoteSession != null) + { + var idx = _remoteSession.Facets.FindIndex(f => f.Index == world.FacetIndex); + if (idx < 0) + return; + BusyWorld = world; + Status = $"Loading {world.Name}..."; + var facet = _remoteSession.Facets[idx]; + new Task(() => ConnectRemoteWorker(facet, world)).Start(); + return; + } + var localFacet = Manager.Facets.Find(f => f.Index == world.FacetIndex); + if (localFacet == null) + return; + BusyWorld = world; + Status = $"Loading {world.Name}..."; + new Task(() => ConnectWorker(localFacet, world)).Start(); + } + + public void FocusFacet(int index) + { + if (_worldsByFacet.TryGetValue(index, out var world) && CEDGame.Worlds.Worlds.Contains(world)) + { + _loadAttempted.Remove(world); + CEDGame.Worlds.RequestFocus(world); + } + } + + public void OpenWorld(Facet facet) + { + if (!facet.DimensionsKnown || !facet.HasStatics) + { + Status = $"{facet.Name}: set its size first."; + return; + } + if (_worldsByFacet.TryGetValue(facet.Index, out var existing) && CEDGame.Worlds.Worlds.Contains(existing)) + { + _loadAttempted.Remove(existing); + CEDGame.Worlds.RequestFocus(existing); + return; + } + var world = CEDGame.Worlds.FindIdleWorld() ?? CEDGame.Worlds.CreateWorld(new Client.CentrEDClient()); + world.Name = facet.Name; + world.FacetIndex = facet.Index; + _worldsByFacet[facet.Index] = world; + CEDGame.Worlds.RequestFocus(world); + } + + private void ConnectWorker(Facet facet, World world) + { + try + { + var clientPath = ProfileManager.ActiveProfile.ClientPath; + world.Map.Load(clientPath); + + var settings = Config.Instance.Facets; + var validFacets = ValidFacets(); + var err = _host.EnsureStarted(validFacets, settings.BasePort, clientPath); + if (err != null) + { + Status = err; + return; + } + var serverIndex = validFacets.FindIndex(f => f.Index == facet.Index); + if (serverIndex < 0 || !_host.HostedFacets.Contains(serverIndex)) + { + Status = $"{facet.Name}: not served by the embedded server."; + return; + } + + world.Client.Connect("127.0.0.1", settings.BasePort, settings.AdminUsername, settings.AdminPassword, serverIndex); + world.Map.TilePosition = new Point(facet.Width * 8 / 2, facet.Height * 8 / 2); + Status = ""; + } + catch (Exception e) + { + Status = $"Failed to open {facet.Name}: {e.Message}"; + Console.WriteLine(e); + } + finally + { + BusyWorld = null; + } + } + + private void ConnectRemoteWorker(Client.ServerFacet facet, World world) + { + try + { + var clientPath = ProfileManager.ActiveProfile.ClientPath; + world.Map.Load(clientPath); + var session = _remoteSession!; + world.Client.Connect(session.Host, session.Port, session.User, session.Pass, facet.Index); + world.Map.TilePosition = new Point(facet.Width * 8 / 2, facet.Height * 8 / 2); + Status = ""; + } + catch (Exception e) + { + Status = $"Failed to open {facet.Name}: {e.Message}"; + Console.WriteLine(e); + } + finally + { + BusyWorld = null; + } + } + + public void CloseWorld(World world) + { + if (world.FacetIndex >= 0) + _worldsByFacet.Remove(world.FacetIndex); + _loadAttempted.Remove(world); + CEDGame.Worlds.Remove(world); + } + + public void Shutdown() => _host.StopAll(); +} diff --git a/CentrED/Map/FacetServerHost.cs b/CentrED/Map/FacetServerHost.cs new file mode 100644 index 00000000..3f487dba --- /dev/null +++ b/CentrED/Map/FacetServerHost.cs @@ -0,0 +1,111 @@ +using System.Net; +using CentrED.Server; +using ServerConfigRoot = CentrED.Server.Config.ConfigRoot; +using ServerMap = CentrED.Server.Config.Map; +using Account = CentrED.Server.Config.Account; +using AccessLevel = CentrED.AccessLevel; + +namespace CentrED.Map; + +public class FacetServerHost +{ + private CEDServer? _server; + private Task? _task; + + public bool Running => _server is { Running: true }; + public bool IsRunning(int index) => Running; + + public IReadOnlyList HostedFacets => + _server?.Landscapes.Select(l => l.FacetIndex).ToList() ?? (IReadOnlyList)Array.Empty(); + + public string? EnsureStarted(IReadOnlyList facets, int port, string assetFallbackFolder) + { + if (Running) + return null; + if (facets.Count == 0) + return "No facets to serve."; + foreach (var facet in facets) + { + if (!facet.DimensionsKnown) + return $"{facet.Name}: map dimensions unknown - set them in CentrED > Options > Facets first."; + if (!facet.HasStatics) + return $"{facet.Name}: matching staidx/statics files were not found."; + } + + Stop(); + try + { + var config = BuildConfig(facets, port, assetFallbackFolder); + _server = new CEDServer(config); + _task = new Task(_server.Run); + _task.Start(); + return null; + } + catch (Exception e) + { + _server = null; + _task = null; + return e.Message; + } + } + + public void Stop() + { + if (_server == null) + return; + try + { + _server.Quit = true; + _task?.Wait(2000); + } + catch + { + } + try { _server.Dispose(); } + catch { } + _server = null; + _task = null; + } + + public void StopAll() => Stop(); + + private static ServerConfigRoot BuildConfig(IReadOnlyList facets, int port, string assetFallbackFolder) + { + var mapDir = Path.GetDirectoryName(facets[0].MapPath) ?? ""; + var config = new ServerConfigRoot + { + CentrEdPlus = false, + Port = port, + BindAddress = IPAddress.Loopback, + Facets = facets.Select(f => new ServerMap + { + Name = f.Name, + MapPath = f.MapPath, + StaIdx = f.StaIdxPath, + Statics = f.StaticsPath, + Width = f.Width, + Height = f.Height, + }).ToList(), + Tiledata = ResolveAsset("tiledata.mul", mapDir, assetFallbackFolder), + Radarcol = ResolveAsset("radarcol.mul", mapDir, assetFallbackFolder), + Hues = ResolveAsset("hues.mul", mapDir, assetFallbackFolder), + }; + var settings = Config.Instance.Facets; + config.Accounts.Add(new Account(settings.AdminUsername, settings.AdminPassword, AccessLevel.Administrator, [])); + return config; + } + + private static string ResolveAsset(string fileName, string mapDir, string fallbackFolder) + { + var primary = Path.Combine(mapDir, fileName); + if (File.Exists(primary)) + return primary; + if (!string.IsNullOrEmpty(fallbackFolder)) + { + var fallback = Path.Combine(fallbackFolder, fileName); + if (File.Exists(fallback)) + return fallback; + } + return fileName; + } +} diff --git a/CentrED/Map/LandObject.cs b/CentrED/Map/LandObject.cs index 3f00ce44..31a4cd30 100644 --- a/CentrED/Map/LandObject.cs +++ b/CentrED/Map/LandObject.cs @@ -29,8 +29,9 @@ public sbyte AverageZ() //TODO Calculate me once } } - public LandObject(LandTile tile) + public LandObject(LandTile tile, MapManager? mapManager = null) { + MapManager = mapManager ?? CEDGame.MapManager; Tile = LandTile = tile; Update(); @@ -44,7 +45,7 @@ public void Update() private bool AlwaysFlat(ushort id) { - ref var tileData = ref CEDGame.MapManager.UoFileManager.TileData.LandData[id]; + ref var tileData = ref MapManager.UoFileManager.TileData.LandData[id]; // Water tiles are always flat return tileData.TexID == 0 || tileData.IsWet; } @@ -56,13 +57,13 @@ private bool IsFlat(float x, float y, float z, float w) public void UpdateCorners(ushort id) { - if (id >= 0x4000 && CEDGame.MapManager.DebugInvalidTiles) + if (id >= 0x4000 && MapManager.DebugInvalidTiles) { Console.WriteLine($"Invalid tile {Tile.Id.FormatId()} at {Tile.X},{Tile.Y}"); id = 1; } var alwaysFlat = AlwaysFlat(id); - var flatView = CEDGame.MapManager.FlatView; + var flatView = MapManager.FlatView; Vector4 cornerZ = flatView ? Vector4.Zero : alwaysFlat ? new Vector4(Tile.Z * TILE_Z_SCALE) : GetCornerZ(); var posX = (Tile.X - 1) * TILE_SIZE; @@ -76,17 +77,17 @@ public void UpdateCorners(ushort id) public void UpdateId(ushort newId) { - if (newId >= 0x4000 && CEDGame.MapManager.DebugInvalidTiles) + if (newId >= 0x4000 && MapManager.DebugInvalidTiles) { Console.WriteLine($"Invalid tile {Tile.Id.FormatId()} at {Tile.X},{Tile.Y}"); newId = 1; } - var mapManager = CEDGame.MapManager; + var mapManager = MapManager; SpriteInfo spriteInfo = default; var isStretched = !IsFlat (Vertices[0].Position.Z, Vertices[1].Position.Z, Vertices[2].Position.Z, Vertices[3].Position.Z); - var isTexMapValid = CEDGame.MapManager.UoFileManager.Texmaps.File.GetValidRefEntry(newId).Length > 0; - var isLandTileValid = CEDGame.MapManager.UoFileManager.Arts.File.GetValidRefEntry(newId).Length > 0; + var isTexMapValid = MapManager.UoFileManager.Texmaps.File.GetValidRefEntry(newId).Length > 0; + var isLandTileValid = MapManager.UoFileManager.Arts.File.GetValidRefEntry(newId).Length > 0; var alwaysFlat = AlwaysFlat(newId); if (mapManager.FlatView) { @@ -107,7 +108,7 @@ public void UpdateId(ushort newId) var useTexMap = !alwaysFlat && isTexMapValid && (Config.Instance.PreferTexMaps || isStretched || !isLandTileValid); if (useTexMap) { - spriteInfo = mapManager.Texmaps.GetTexmap(CEDGame.MapManager.UoFileManager.TileData.LandData[newId].TexID); + spriteInfo = mapManager.Texmaps.GetTexmap(MapManager.UoFileManager.TileData.LandData[newId].TexID); } else { @@ -119,7 +120,7 @@ public void UpdateId(ushort newId) if(mapManager.DebugLogging) Console.WriteLine($"No texture found for land {Tile.X},{Tile.Y},{Tile.Z}:0x{newId:X}, texmap:{useTexMap}"); //VOID texture is by default all pink, so it should be noticeable that something is not right - spriteInfo = CEDGame.MapManager.Texmaps.GetTexmap(0x0001); + spriteInfo = MapManager.Texmaps.GetTexmap(0x0001); } Texture = spriteInfo.Texture; @@ -155,7 +156,7 @@ public void UpdateId(ushort newId) private Vector4 GetCornerZ() { var client = CEDClient; - var mapManager = CEDGame.MapManager; + var mapManager = MapManager; var x = Tile.X; var y = Tile.Y; var top = IsGhost ? @@ -179,7 +180,7 @@ private Vector4 GetCornerZ() private bool CalculateNormals(out Vector3[] normals) { normals = new Vector3[4]; - var mapManager = CEDGame.MapManager; + var mapManager = MapManager; var x = Tile.X; var y = Tile.Y; /* _____ _____ _____ _____ diff --git a/CentrED/Map/MapManager.cs b/CentrED/Map/MapManager.cs index 2a5b4346..896892bd 100644 --- a/CentrED/Map/MapManager.cs +++ b/CentrED/Map/MapManager.cs @@ -45,25 +45,39 @@ public class MapManager public bool DebugDrawSelectionBuffer; private RenderTarget2D _lightMap; public bool DebugDrawLightMap; + private RenderTarget2D _worldRenderTarget; + public RenderTarget2D Output => _worldRenderTarget; + public int ViewMouseX, ViewMouseY; + public bool ViewHovered, ViewFocused; + public bool WindowVisible; + private int _prevMouseX, _prevMouseY; public MapEffect MapEffect { get; private set; } - public UOFileManager UoFileManager { get; private set; } - private AnimatedStaticsManager _animatedStaticsManager; - public Art Arts; - public Texmap Texmaps; - public BlueprintManager BlueprintManager; - - internal List Tools = []; - private Tool _activeTool; + private static UOFileManager _assets; + private static AnimatedStaticsManager _animatedStaticsManager; + private static Art _sharedArts; + private static Texmap _sharedTexmaps; + private static BlueprintManager _sharedBlueprints; + private static string _loadedClientPath = ""; + private static TileDataLand[] _sharedLandTileData = []; + private static TileDataStatic[] _sharedStaticTileData = []; + public UOFileManager UoFileManager => _assets; + public Art Arts => _sharedArts; + public Texmap Texmaps => _sharedTexmaps; + public BlueprintManager BlueprintManager => _sharedBlueprints; + + private static readonly List _sharedTools = []; + internal List Tools => _sharedTools; + private static Tool _activeTool; public Tool ActiveTool { get => _activeTool; set { - _activeTool.OnMouseLeave(Selected); - _activeTool.OnDeactivated(Selected); + _activeTool?.OnMouseLeave(Selected); + _activeTool?.OnDeactivated(Selected); _activeTool = value; _activeTool.OnActivated(Selected); _activeTool.OnMouseEnter(Selected); @@ -107,15 +121,18 @@ public Tool ActiveTool public bool ObjectHueFilterInclusive = true; public HashSet _ToRecalculate = new(); - public List ValidLandIds { get; } = []; - public List ValidStaticIds { get; } = []; + private static readonly List _sharedValidLandIds = []; + private static readonly List _sharedValidStaticIds = []; + public List ValidLandIds => _sharedValidLandIds; + public List ValidStaticIds => _sharedValidStaticIds; - public MapManager(GraphicsDevice gd, GameWindow window, Keymap keymap) + public MapManager(GraphicsDevice gd, GameWindow window, Keymap keymap, CentrEDClient client) { _gfxDevice = gd; _gameWindow = window; _keymap = keymap; - + StaticsManager.MapManager = this; + MapEffect = new MapEffect(gd); _mapRenderer = new MapRenderer(gd, window); _spriteBatch = new SpriteBatch(gd); @@ -128,7 +145,7 @@ public MapManager(GraphicsDevice gd, GameWindow window, Keymap keymap) _background = Texture2D.FromStream(gd, fileStream); } - Client = CEDClient; + Client = client; Client.Connected += OnConnected; Client.Disconnected += OnDisconnected; EnableBlockLoading(); @@ -148,20 +165,22 @@ public MapManager(GraphicsDevice gd, GameWindow window, Keymap keymap) Client.LoggedError += Console.WriteLine; #endif - Tools.Add(new SelectTool()); //Select tool have to be first! - Tools.Add(new DrawTool()); - Tools.Add(new MoveTool()); - Tools.Add(new ElevateTool()); - Tools.Add(new DeleteTool()); - Tools.Add(new HueTool()); - Tools.Add(new LandBrushTool()); - Tools.Add(new MeshEditTool()); - Tools.Add(new AltitudeGradientTool()); - Tools.Add(new CoastlineTool()); - - Tools.ForEach(t => t.PostConstruct(this)); - - _activeTool = Tools[0]; + if (_sharedTools.Count == 0) + { + Tools.Add(new SelectTool()); //Select tool have to be first! + Tools.Add(new DrawTool()); + Tools.Add(new MoveTool()); + Tools.Add(new ElevateTool()); + Tools.Add(new DeleteTool()); + Tools.Add(new HueTool()); + Tools.Add(new LandBrushTool()); + Tools.Add(new MeshEditTool()); + Tools.Add(new AltitudeGradientTool()); + Tools.Add(new CoastlineTool()); + + Tools.ForEach(t => t.PostConstruct(this)); + _activeTool = Tools[0]; + } OnWindowsResized(window); } @@ -282,7 +301,7 @@ private void AfterStaticChanged(StaticTile tile) private void AddTile(LandTile landTile) { - var lo = new LandObject(landTile); + var lo = new LandObject(landTile, this); LandTiles[landTile.X, landTile.Y] = lo; LandTilesIdDictionary.Add(lo.ObjectId, lo); LandTilesCount++; @@ -296,6 +315,14 @@ public void ReloadShader() public void Load(string clientPath) { + LoadSharedAssets(clientPath); + Client.InitTileData(_sharedLandTileData, _sharedStaticTileData); + } + + private void LoadSharedAssets(string clientPath) + { + if (_loadedClientPath == clientPath) + return; var tiledataFile = Path.Combine(clientPath, "tiledata.mul"); var clientVersion = new FileInfo(tiledataFile).Length switch { @@ -303,53 +330,55 @@ public void Load(string clientPath) >= 1644544 => ClientVersion.CV_7000, _ => ClientVersion.CV_6000 }; - UoFileManager = new UOFileManager(clientVersion, clientPath); - //We don't UoFileManager.Load() as we don't need all the assets - UoFileManager.Arts.Load(); - UoFileManager.Hues.Load(); - UoFileManager.TileData.Load(); - UoFileManager.Texmaps.Load(); - UoFileManager.AnimData.Load(); - UoFileManager.Lights.Load(); - UoFileManager.Multis.Load(); - + _assets = new UOFileManager(clientVersion, clientPath); + //We don't _assets.Load() as we don't need all the assets + _assets.Arts.Load(); + _assets.Hues.Load(); + _assets.TileData.Load(); + _assets.Texmaps.Load(); + _assets.AnimData.Load(); + _assets.Lights.Load(); + _assets.Multis.Load(); + _animatedStaticsManager = new AnimatedStaticsManager(); _animatedStaticsManager.Initialize(); - Arts = new Art(UoFileManager.Arts, UoFileManager.Hues, _gfxDevice); - Texmaps = new Texmap(UoFileManager.Texmaps, _gfxDevice); + _sharedArts = new Art(_assets.Arts, _assets.Hues, _gfxDevice); + _sharedTexmaps = new Texmap(_assets.Texmaps, _gfxDevice); HuesManager.Load(_gfxDevice); LightsManager.Load(_gfxDevice); + NonWalkableHue = HuesManager.Instance.GetRGBVector(Color.FromArgb(50, 0, 0)); + WalkableHue = HuesManager.Instance.GetRGBVector(Color.FromArgb(0, 50, 0)); - var tdl = UoFileManager.TileData; - ValidLandIds.Clear(); + var tdl = _assets.TileData; + _sharedValidLandIds.Clear(); for (var i = 0; i < tdl.LandData.Length; i++) { - var isArtValid = CEDGame.MapManager.UoFileManager.Arts.File.GetValidRefEntry(i).Length > 0; + var isArtValid = _assets.Arts.File.GetValidRefEntry(i).Length > 0; var texId = tdl.LandData[i].TexID; - var isTexValid = CEDGame.MapManager.UoFileManager.Texmaps.File.GetValidRefEntry(texId).Length > 0; + var isTexValid = _assets.Texmaps.File.GetValidRefEntry(texId).Length > 0; // Only show tiles if art OR texture is valid if (isArtValid || isTexValid) { - ValidLandIds.Add((ushort)i); + _sharedValidLandIds.Add((ushort)i); } } - ValidStaticIds.Clear(); + _sharedValidStaticIds.Clear(); for (var i = 0; i < tdl.StaticData.Length; i++) { - if (!UoFileManager.Arts.File.GetValidRefEntry(i + ArtLoader.MAX_LAND_DATA_INDEX_COUNT).Equals + if (!_assets.Arts.File.GetValidRefEntry(i + ArtLoader.MAX_LAND_DATA_INDEX_COUNT).Equals (UOFileIndex.Invalid)) { - ValidStaticIds.Add((ushort)i); + _sharedValidStaticIds.Add((ushort)i); } } - var landTileData = tdl.LandData.Select(ltd => new TileDataLand((ulong)ltd.Flags, ltd.TexID, ltd.Name)).ToArray(); - var staticTileData = tdl.StaticData.Select(std => new TileDataStatic((ulong)std.Flags, std.Weight, std.Layer, std.Count, std.AnimID, std.Hue, std.LightIndex, std.Height, std.Name)).ToArray(); - Client.InitTileData(landTileData, staticTileData); + _sharedLandTileData = tdl.LandData.Select(ltd => new TileDataLand((ulong)ltd.Flags, ltd.TexID, ltd.Name)).ToArray(); + _sharedStaticTileData = tdl.StaticData.Select(std => new TileDataStatic((ulong)std.Flags, std.Weight, std.Layer, std.Count, std.AnimID, std.Hue, std.LightIndex, std.Height, std.Name)).ToArray(); - BlueprintManager = new BlueprintManager(UoFileManager.Multis); - BlueprintManager.Load(); + _sharedBlueprints = new BlueprintManager(_assets.Multis); + _sharedBlueprints.Load(); + _loadedClientPath = clientPath; } public Vector2 Position @@ -391,6 +420,14 @@ public static Vector2 ScreenToMapCoordinates(float x, float y) public int LandTilesCount; public Dictionary GhostLandTiles = new(); + private LandObject? GetLandObject(int x, int y) + { + var tiles = LandTiles; + if ((uint)x >= (uint)tiles.GetLength(0) || (uint)y >= (uint)tiles.GetLength(1)) + return null; + return tiles[x, y]; + } + public StaticsManager StaticsManager = new(); public VirtualLayerObject VirtualLayer = VirtualLayerObject.Instance; //Used for drawing public ImageOverlay ImageOverlay = new(); //Used for image overlay feature @@ -409,11 +446,7 @@ public void UpdateAllTiles() public LandTile? GetLandTile(int x, int y) { - if (!Client.IsValidX(x) || !Client.IsValidY(y)) - { - return null; - } - var realLandTile = LandTiles[x, y]; + var realLandTile = GetLandObject(x, y); if (realLandTile == null) return null; if (GhostLandTiles.TryGetValue(realLandTile, out var ghostLandTile)) @@ -507,12 +540,13 @@ public void Update(GameTime gameTime, bool isActive, bool processMouse, bool pro { if (CEDGame.Closing) return; - if (CEDClient.ServerState != ServerState.Running) + if (Client.ServerState != ServerState.Running) return; Metrics.Start("UpdateMap"); var mouseState = Mouse.GetState(); - if (processMouse) + var isActiveWorld = ReferenceEquals(this, CEDGame.Worlds.Active?.Map); + if (isActiveWorld && processMouse) { if (Client.Running) { @@ -544,7 +578,7 @@ public void Update(GameTime gameTime, bool isActive, bool processMouse, bool pro } if (mouseState.RightButton == ButtonState.Pressed) { - var mouseDelta = new Vector2(_prevMouseState.X - mouseState.X, _prevMouseState.Y - mouseState.Y); + var mouseDelta = new Vector2(_prevMouseX - ViewMouseX, _prevMouseY - ViewMouseY); if (mouseDelta != Vector2.Zero) { var moveOffset = ScreenToMapCoordinates(mouseDelta.X, mouseDelta.Y) / Camera.Zoom; @@ -565,7 +599,7 @@ public void Update(GameTime gameTime, bool isActive, bool processMouse, bool pro } if (mouseState.MiddleButton == ButtonState.Pressed) { - var mouseDelta = new Vector2(_prevMouseState.X - mouseState.X, _prevMouseState.Y - mouseState.Y); + var mouseDelta = new Vector2(_prevMouseX - ViewMouseX, _prevMouseY - ViewMouseY); if (mouseDelta != Vector2.Zero) { var mod = 0.5f; @@ -593,15 +627,17 @@ public void Update(GameTime gameTime, bool isActive, bool processMouse, bool pro ActiveTool.OnMouseReleased(Selected); } } - else + else if (isActiveWorld) { ActiveTool.OnMouseLeave(PrevSelected); ActiveTool.OnMouseReleased(PrevSelected); Selected = null; } _prevMouseState = mouseState; + _prevMouseX = ViewMouseX; + _prevMouseY = ViewMouseY; - if (processKeyboard) + if (isActiveWorld && processKeyboard) { foreach (var key in _keymap.GetKeysReleased()) { @@ -723,7 +759,8 @@ public void Update(GameTime gameTime, bool isActive, bool processMouse, bool pro } if (Client.Running && AnimatedStatics) { - _animatedStaticsManager.Process(gameTime); + if (this == CEDGame.Worlds.Active?.Map) + _animatedStaticsManager.Process(gameTime); foreach (var animatedStaticTile in StaticsManager.AnimatedTiles) { animatedStaticTile.UpdateId(); @@ -771,11 +808,7 @@ public void UpdateLights() private void UpdateMouseSelection(int x, int y) { - if (!_selectionBuffer.Bounds.Contains(x, y)) - { - RealSelected = null; - } - else if (CEDGame.UIManager.IsOverUI(x, y)) + if (!ViewHovered || !_selectionBuffer.Bounds.Contains(x, y)) { RealSelected = null; } @@ -833,7 +866,7 @@ private RectU16 CalculateViewRange(Camera camera) public Vector3 Unproject(int x, int y, int z) { - var worldPoint = _gfxDevice.Viewport.Unproject + var worldPoint = ViewportFromCamera().Unproject ( new FNAVector3(x, y, -(z / 384f) + 0.5f), Camera.FnaWorldViewProj, @@ -917,8 +950,8 @@ public bool CanDrawStatic(StaticObject so) return show; } - private static Vector4 NonWalkableHue = HuesManager.Instance.GetRGBVector(Color.FromArgb(50, 0, 0)); - private static Vector4 WalkableHue = HuesManager.Instance.GetRGBVector(Color.FromArgb(0, 50, 0)); + private static Vector4 NonWalkableHue; + private static Vector4 WalkableHue; public Vector4 GhostLandTilesHue = Vector4.Zero; public bool IsWalkable(LandObject lo) @@ -1014,16 +1047,16 @@ public void Draw() } Metrics.Measure("DrawSelection", DrawSelectionBuffer); Metrics.Start("GetMouseSelection"); - UpdateMouseSelection(_prevMouseState.X, _prevMouseState.Y); + UpdateMouseSelection(ViewMouseX, ViewMouseY); Metrics.Stop("GetMouseSelection"); if (DebugDrawSelectionBuffer) return; - + Metrics.Measure("DrawLights", () => DrawLights(Camera)); if (DebugDrawLightMap) return; - - _mapRenderer.SetRenderTarget(null); + + _mapRenderer.SetRenderTarget(_worldRenderTarget, new FNARectangle(0, 0, _worldRenderTarget.Width, _worldRenderTarget.Height)); Metrics.Measure("DrawImageOverlayBelow", () => DrawImageOverlay(false)); Metrics.Measure("DrawLand", () => DrawLand(Camera, ViewRange)); Metrics.Start("DrawLandGrid"); @@ -1051,15 +1084,13 @@ public void AfterDraw() private void DrawBackground() { - _mapRenderer.SetRenderTarget(null); - _gfxDevice.Clear(FNAColor.Black); + _mapRenderer.SetRenderTarget(_worldRenderTarget, new FNARectangle(0, 0, _worldRenderTarget.Width, _worldRenderTarget.Height)); _gfxDevice.BlendState = BlendState.AlphaBlend; _spriteBatch.Begin(); - var windowRect = _gameWindow.ClientBounds; var backgroundRect = new FNARectangle ( - windowRect.Width / 2 - _background.Width / 2, - windowRect.Height / 2 - _background.Height / 2, + _worldRenderTarget.Width / 2 - _background.Width / 2, + _worldRenderTarget.Height / 2 - _background.Height / 2, _background.Width, _background.Height ); @@ -1071,7 +1102,8 @@ private void DrawSelectionBuffer() { MapEffect.WorldViewProj = Camera.FnaWorldViewProj; MapEffect.CurrentTechnique = MapEffect.Techniques["Selection"]; - _mapRenderer.SetRenderTarget(DebugDrawSelectionBuffer ? null : _selectionBuffer); + _mapRenderer.SetRenderTarget(DebugDrawSelectionBuffer ? null : _selectionBuffer, + new FNARectangle(0, 0, _selectionBuffer.Width, _selectionBuffer.Height)); _mapRenderer.Begin ( MapEffect, @@ -1082,7 +1114,7 @@ private void DrawSelectionBuffer() ); foreach (var (x,y) in ViewRange.Iterate()) { - var landTile = LandTiles[x, y]; + var landTile = GetLandObject(x, y); if (landTile != null) { DrawLand(landTile, landTile.ObjectIdColor); @@ -1109,7 +1141,8 @@ private void DrawLights(Camera camera) } MapEffect.WorldViewProj = camera.FnaWorldViewProj; MapEffect.CurrentTechnique = MapEffect.Techniques["Statics"]; - _mapRenderer.SetRenderTarget(DebugDrawLightMap ? null : _lightMap); + _mapRenderer.SetRenderTarget(DebugDrawLightMap ? null : _lightMap, + new FNARectangle(0, 0, _lightMap.Width, _lightMap.Height)); _gfxDevice.Clear(ClearOptions.Target, LightsManager.Instance.GlobalLightLevelColor, 0f, 0); _mapRenderer.Begin ( @@ -1153,7 +1186,7 @@ private void DrawLand(Camera camera, RectU16 viewRange, string technique = "Terr foreach (var (x,y) in viewRange.Iterate()) { - var tile = LandTiles[x, y]; + var tile = GetLandObject(x, y); if (tile != null && tile.CanDraw) { var hueOverride = Vector4.Zero; @@ -1184,7 +1217,7 @@ private void DrawLandHeight() _spriteBatch.Begin(); foreach (var (x, y) in ViewRange.Iterate()) { - var tile = LandTiles[x, y]; + var tile = GetLandObject(x, y); if (tile != null && tile.CanDraw) { DrawTileHeight(tile, font, halfTile); @@ -1202,13 +1235,12 @@ private void DrawTileHeight(LandObject tile, DynamicSpriteFont font, float yOffs var text = tile.LandTile.Z.ToString(); var halfTextSize = font.MeasureString(text) / 2; var tilePos = tile.Vertices[0].Position; - var projected = _gfxDevice.Viewport.Project + var projected = ViewportFromCamera().Project (new FNAVector3(tilePos.X, tilePos.Y, tilePos.Z), Camera.FnaWorldViewProj, Matrix.Identity, Matrix.Identity); var pos = new Vector2 (projected.X - halfTextSize.X, projected.Y + yOffset); - var windowRect = _gameWindow.ClientBounds; - if (pos.X > 0 && pos.X < windowRect.Width && pos.Y > 0 && - pos.Y < windowRect.Height) + if (pos.X > 0 && pos.X < Camera.ScreenSize.Width && pos.Y > 0 && + pos.Y < Camera.ScreenSize.Height) { _spriteBatch.DrawString(font, text, new FNAVector2(pos.X, pos.Y), FNAColor.White); } @@ -1381,29 +1413,42 @@ public void ExportImage() public void OnWindowsResized(GameWindow window) { var windowSize = window.ClientBounds; - Camera.ScreenSize = new Rectangle(windowSize.X, windowSize.Y, windowSize.Width, windowSize.Height); + Resize(windowSize.Width, windowSize.Height); + } + + private int _pendingWidth, _pendingHeight; + + public void Resize(int width, int height) + { + width = Math.Max(1, width); + height = Math.Max(1, height); + if (_worldRenderTarget != null && Camera.ScreenSize.Width == width && Camera.ScreenSize.Height == height) + return; + if (_worldRenderTarget != null && (width != _pendingWidth || height != _pendingHeight)) + { + _pendingWidth = width; + _pendingHeight = height; + return; + } + + Camera.ScreenSize = new Rectangle(0, 0, width, height); Camera.Update(); _selectionBuffer?.Dispose(); - _selectionBuffer = new RenderTarget2D - ( - _gfxDevice, - windowSize.Width, - windowSize.Height, - false, - SurfaceFormat.Color, - DepthFormat.Depth24 - ); + _selectionBuffer = new RenderTarget2D(_gfxDevice, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24); _lightMap?.Dispose(); - _lightMap = new RenderTarget2D - ( - _gfxDevice, - windowSize.Width, - windowSize.Height, - - false, - SurfaceFormat.Color, - DepthFormat.None - ); + _lightMap = new RenderTarget2D(_gfxDevice, width, height, false, SurfaceFormat.Color, DepthFormat.None); + _worldRenderTarget?.Dispose(); + _worldRenderTarget = new RenderTarget2D(_gfxDevice, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24); + } + + private Viewport ViewportFromCamera() => new(0, 0, Camera.ScreenSize.Width, Camera.ScreenSize.Height); + + public void Dispose() + { + DisableBlockLoading(); + _selectionBuffer?.Dispose(); + _lightMap?.Dispose(); + _worldRenderTarget?.Dispose(); } } \ No newline at end of file diff --git a/CentrED/Map/RadarMap.cs b/CentrED/Map/RadarMap.cs index e7da6edf..28c4a9f6 100644 --- a/CentrED/Map/RadarMap.cs +++ b/CentrED/Map/RadarMap.cs @@ -1,57 +1,69 @@ -using CentrED.Client; +using CentrED.Client; using ClassicUO.Utility; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using static CentrED.Application; namespace CentrED.Map; public class RadarMap { - private static RadarMap _instance; - public static RadarMap Instance => _instance; + private readonly GraphicsDevice _gd; + private readonly CentrEDClient _client; + private Texture2D? _texture; + public Texture2D? Texture => _texture; + public bool IsReady => _texture != null; - private Texture2D _texture = null!; - public Texture2D Texture => _texture; - - private RadarMap(GraphicsDevice gd) + public RadarMap(GraphicsDevice gd, CentrEDClient client) { - CEDClient.Connected += () => - { - _texture = new Texture2D(gd, CEDClient.Width, CEDClient.Height); - CEDClient.Send(new RequestRadarMapPacket()); - }; + _gd = gd; + _client = client; + client.Connected += OnConnected; + client.RadarData += RadarData; + client.RadarUpdate += RadarUpdate; + } - CEDClient.RadarData += RadarData; - CEDClient.RadarUpdate += RadarUpdate; + public void Refresh() + { + if (_client.Running) + _client.Send(new RequestRadarMapPacket()); } - public static void Initialize(GraphicsDevice gd) + private void OnConnected() { - _instance = new RadarMap(gd); + _texture = new Texture2D(_gd, _client.Width, _client.Height); + _client.Send(new RequestRadarMapPacket()); } private unsafe void RadarData(ReadOnlySpan data) { - var width = CEDClient.Width; - var height = CEDClient.Height; + if (_texture == null) + return; + var width = _client.Width; + var height = _client.Height; uint[] buffer = System.Buffers.ArrayPool.Shared.Rent(data.Length); - for (ushort x = 0; x < width; x++) + try { - for (ushort y = 0; y < height; y++) + for (ushort x = 0; x < width; x++) { - buffer[y * width + x] = HuesHelper.Color16To32(data[x * height + y]) | 0xFF_00_00_00; + for (ushort y = 0; y < height; y++) + { + buffer[y * width + x] = HuesHelper.Color16To32(data[x * height + y]) | 0xFF_00_00_00; + } } - } - fixed (uint* ptr = buffer) + fixed (uint* ptr = buffer) + { + _texture.SetDataPointerEXT(0, null, (IntPtr)ptr, data.Length * sizeof(uint)); + } + } + finally { - _texture.SetDataPointerEXT(0, null, (IntPtr)ptr, data.Length * sizeof(uint)); + System.Buffers.ArrayPool.Shared.Return(buffer); } } private void RadarUpdate(ushort x, ushort y, ushort color) { - _texture.SetData(0, new Rectangle(x, y, 1, 1), new[] { HuesHelper.Color16To32(color) | 0xFF_00_00_00 }, 0, 1); + _texture?.SetData(0, new Rectangle(x, y, 1, 1), new[] { HuesHelper.Color16To32(color) | 0xFF_00_00_00 }, 0, 1); } -} \ No newline at end of file +} diff --git a/CentrED/Map/StaticObject.cs b/CentrED/Map/StaticObject.cs index 53270ed7..32bacc14 100644 --- a/CentrED/Map/StaticObject.cs +++ b/CentrED/Map/StaticObject.cs @@ -15,13 +15,14 @@ public class StaticObject : TileObject, IComparable public bool IsLight; public Rectangle RealBounds; - public StaticObject(StaticTile tile) + public StaticObject(StaticTile tile, MapManager? mapManager = null) { + MapManager = mapManager ?? CEDGame.MapManager; //Static are constructed from two rectangles Vertices = new MapVertex[8]; Tile = StaticTile = tile; - - var realBounds = CEDGame.MapManager.Arts.GetRealArtBounds(Tile.Id); + + var realBounds = MapManager.Arts.GetRealArtBounds(Tile.Id); RealBounds = new Rectangle(realBounds.X, realBounds.Y, realBounds.Width, realBounds.Height); UpdateId(Tile.Id); UpdatePos(tile.X, tile.Y, tile.Z); @@ -30,7 +31,7 @@ public StaticObject(StaticTile tile) { Vertices[i].Normal = Vector3.Zero; } - var tiledata = CEDGame.MapManager.UoFileManager.TileData.StaticData[Tile.Id]; + var tiledata = MapManager.UoFileManager.TileData.StaticData[Tile.Id]; IsAnimated = tiledata.IsAnimated; IsLight = tiledata.IsLight; } @@ -48,7 +49,7 @@ public void UpdateId() public void UpdateId(ushort newId) { - var mapManager = CEDGame.MapManager; + var mapManager = MapManager; ref var index = ref mapManager.UoFileManager.Arts.File.GetValidRefEntry(newId + 0x4000); var spriteInfo = mapManager.Arts.GetArt((uint)(newId + index.AnimOffset)); if (spriteInfo.Equals(SpriteInfo.Empty)) @@ -97,7 +98,7 @@ public void UpdatePos(ushort newX, ushort newY, sbyte newZ) { var posX = newX * TILE_SIZE; var posY = newY * TILE_SIZE; - var posZ = CEDGame.MapManager.FlatView ? 0 : newZ * TILE_Z_SCALE; + var posZ = MapManager.FlatView ? 0 : newZ * TILE_Z_SCALE; float projectedWidth = TextureBounds.Width * RSQRT2; float halfWidth = TextureBounds.Width * 0.5f; diff --git a/CentrED/Map/StaticsManager.cs b/CentrED/Map/StaticsManager.cs index bc7092bd..71464c78 100644 --- a/CentrED/Map/StaticsManager.cs +++ b/CentrED/Map/StaticsManager.cs @@ -6,6 +6,8 @@ namespace CentrED.Map; public class StaticsManager { private static readonly ReadOnlyCollection EMPTY = []; + + public MapManager MapManager = null!; private ushort _Width; private ushort _Height; @@ -62,7 +64,7 @@ public ReadOnlyCollection Get(int x, int y) public ReadOnlyCollection Get(ushort x, ushort y) { - if (x > _Width || y > _Height) + if (x >= _Width || y >= _Height) return EMPTY; var list = _tiles[Index(x, y)]; return list?.AsReadOnly() ?? EMPTY; @@ -78,7 +80,7 @@ public ReadOnlyCollection Get(ushort x, ushort y) public void Add(StaticTile staticTile) { - var so = new StaticObject(staticTile); + var so = new StaticObject(staticTile, MapManager); var index = Index(staticTile); var list = _tiles[index]; if (list == null) diff --git a/CentrED/Map/TileObject.cs b/CentrED/Map/TileObject.cs index ec742a1a..b67eb71a 100644 --- a/CentrED/Map/TileObject.cs +++ b/CentrED/Map/TileObject.cs @@ -4,6 +4,7 @@ public abstract class TileObject : MapObject { public BaseTile Tile; public bool? Walkable; + public MapManager MapManager = null!; public virtual void Reset() { diff --git a/CentrED/Map/World.cs b/CentrED/Map/World.cs new file mode 100644 index 00000000..6d699d45 --- /dev/null +++ b/CentrED/Map/World.cs @@ -0,0 +1,96 @@ +using CentrED.Client; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using static CentrED.Application; + +namespace CentrED.Map; + +public class World +{ + public CentrEDClient Client { get; } + public MapManager Map { get; } + public RadarMap RadarMap { get; } + public string Name = "World"; + public int FacetIndex = -1; + + public World(GraphicsDevice gd, GameWindow window, Keymap keymap, CentrEDClient client) + { + Client = client; + Map = new MapManager(gd, window, keymap, client); + RadarMap = new RadarMap(gd, client); + } + + public void Close() + { + try + { + if (Client.Running) + Client.Disconnect(); + } + catch + { + } + Map.Dispose(); + } +} + +public class WorldManager +{ + private GraphicsDevice _gd = null!; + private GameWindow _window = null!; + private Keymap _keymap = null!; + + private readonly List _worlds = new(); + public IReadOnlyList Worlds => _worlds; + public World? Active { get; private set; } + public CentrEDClient? ActiveClient => Active?.Client; + public World? Focused { get; private set; } + public World? FocusRequest; + + public void Initialize(GraphicsDevice gd, GameWindow window, Keymap keymap) + { + _gd = gd; + _window = window; + _keymap = keymap; + } + + public World CreateWorld(CentrEDClient client) + { + var world = new World(_gd, _window, _keymap, client); + _worlds.Add(world); + Active ??= world; + return world; + } + + public World? FindIdleWorld() => _worlds.Find(w => w.FacetIndex < 0 && !w.Client.Running); + + public void SetActive(World world) + { + if (_worlds.Contains(world)) + Active = world; + } + + public void SetFocused(World world) + { + if (_worlds.Contains(world)) + Focused = world; + } + + public void RequestFocus(World world) + { + SetActive(world); + SetFocused(world); + FocusRequest = world; + } + + public void Remove(World world) + { + if (!_worlds.Remove(world)) + return; + world.Close(); + if (Active == world) + Active = _worlds.Count > 0 ? _worlds[0] : null; + if (Focused == world) + Focused = Active; + } +} diff --git a/CentrED/UI/UIManager.cs b/CentrED/UI/UIManager.cs index 7e221033..2f150378 100644 --- a/CentrED/UI/UIManager.cs +++ b/CentrED/UI/UIManager.cs @@ -280,6 +280,7 @@ public void OpenContextMenu(TileObject? selected) } private bool _resetLayout; + private uint _dockSpaceId; protected virtual void DrawUI() { @@ -293,9 +294,11 @@ protected virtual void DrawUI() { ImGui.LoadIniSettingsFromDisk("imgui.ini.default"); Config.Instance.Layout = new Dictionary(); + ResetWorldDocking(); _resetLayout = false; } - ImGui.DockSpaceOverViewport(ImGuiDockNodeFlags.PassthruCentralNode | ImGuiDockNodeFlags.NoDockingOverCentralNode); + _dockSpaceId = ImGui.DockSpaceOverViewport(ImGuiDockNodeFlags.None); + DrawWorldArea(); DrawContextMenu(); DrawMainMenu(); DrawStatusBar(); @@ -428,11 +431,184 @@ private void DrawMainMenu() DebugWindow.DrawMenuItem(); ImGui.EndMenu(); } + DrawWorldsMenu(); CEDGame.UIManager.AddCurrentWindowRect(); ImGui.EndMainMenuBar(); } } + private readonly Dictionary _worldBindings = new(); + private readonly HashSet _redockRequested = new(); + private uint _worldsDockId; + + private static string WorldWindowId(World world) => world.FacetIndex >= 0 ? $"facet_{world.FacetIndex}" : "world"; + + public void ResetWorldDocking() + { + if (CEDGame?.Worlds == null) + return; + foreach (var world in CEDGame.Worlds.Worlds) + _redockRequested.Add(world); + } + + private unsafe void DrawWorldArea() + { + var worlds = CEDGame.Worlds; + if (worlds == null) + return; + + foreach (var (world, binding) in _worldBindings.Where(kv => !worlds.Worlds.Contains(kv.Key)).ToList()) + { + _uiRenderer.UnbindTexture(binding.Id); + _worldBindings.Remove(world); + } + _redockRequested.RemoveWhere(w => !worlds.Worlds.Contains(w)); + + Vector2 hostPos, hostSize; + var central = _dockSpaceId != 0 ? ImGuiP.DockBuilderGetCentralNode(_dockSpaceId) : default; + if (!central.IsNull) + { + hostPos = central.Pos; + hostSize = central.Size; + } + else + { + var vp = ImGui.GetMainViewport(); + hostPos = vp.WorkPos; + hostSize = vp.WorkSize; + } + ImGui.SetNextWindowPos(hostPos); + ImGui.SetNextWindowSize(hostSize); + var hostFlags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoScrollbar | + ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoNavFocus; + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); + if (ImGui.Begin("###WorldHost", hostFlags)) + { + _worldsDockId = ImGui.GetID("WorldsDockSpace"); + const ImGuiDockNodeFlags worldsDockFlags = ImGuiDockNodeFlags.AutoHideTabBar | (ImGuiDockNodeFlags)(1 << 15); + ImGui.DockSpace(_worldsDockId, new Vector2(0, 0), worldsDockFlags); + } + ImGui.End(); + ImGui.PopStyleVar(); + + World? hoveredWorld = null; + World? focusedWorld = null; + for (var i = 0; i < worlds.Worlds.Count; i++) + { + var world = worlds.Worlds[i]; + var map = world.Map; + if (world == worlds.FocusRequest) + { + ImGui.SetNextWindowFocus(); + worlds.FocusRequest = null; + } + var flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse; + var title = $"{world.Name}###{WorldWindowId(world)}"; + var dockCond = _redockRequested.Remove(world) ? ImGuiCond.Always : ImGuiCond.FirstUseEver; + ImGui.SetNextWindowDockID(_worldsDockId, dockCond); + var open = true; + var begun = ImGui.Begin(title, ref open, flags); + map.WindowVisible = begun; + if (begun) + { + var avail = ImGui.GetContentRegionAvail(); + map.Resize((int)avail.X, (int)avail.Y); + + var contentPos = ImGui.GetCursorScreenPos(); + var output = map.Output; + if (output != null) + { + if (_worldBindings.TryGetValue(world, out var prev) && prev.Tex != output) + _uiRenderer.UnbindTexture(prev.Id); + var id = _uiRenderer.BindTexture(output); + _worldBindings[world] = (output, id); + ImGui.Image(new ImTextureRef(null, id), new Vector2(Math.Max(1, avail.X), Math.Max(1, avail.Y))); + map.ViewHovered = ImGui.IsItemHovered(); + } + else + { + map.ViewHovered = false; + } + + var mouse = ImGui.GetMousePos(); + map.ViewMouseX = (int)(mouse.X - contentPos.X); + map.ViewMouseY = (int)(mouse.Y - contentPos.Y); + if (map.ViewHovered && + (ImGui.IsMouseClicked(ImGuiMouseButton.Left) || ImGui.IsMouseClicked(ImGuiMouseButton.Right))) + ImGui.SetWindowFocus(); + map.ViewFocused = ImGui.IsWindowFocused(); + if (map.ViewHovered) + hoveredWorld = world; + if (map.ViewFocused) + { + focusedWorld = world; + worlds.SetFocused(world); + } + } + else + { + map.ViewHovered = false; + map.ViewFocused = false; + } + ImGui.End(); + if (!open) + _redockRequested.Add(world); + } + if (hoveredWorld != null) + worlds.SetActive(hoveredWorld); + else if (focusedWorld != null) + worlds.SetActive(focusedWorld); + } + + private void DrawWorldsMenu() + { + var controller = CEDGame.FacetController; + if (controller == null) + return; + + if (controller.InRemoteMode) + { + if (!ImGui.BeginMenu("Worlds")) + return; + var activeRemote = CEDGame.Worlds.Active?.FacetIndex ?? -1; + foreach (var facet in controller.RemoteFacets) + { + var name = string.IsNullOrEmpty(facet.Name) ? $"Facet {facet.Index}" : facet.Name; + if (ImGui.MenuItem(name, "", facet.Index == activeRemote)) + controller.FocusFacet(facet.Index); + } + ImGui.EndMenu(); + return; + } + + if (controller.Facets.Count == 0) + return; + if (!ImGui.BeginMenu("Worlds")) + return; + + var activeFacet = CEDGame.Worlds.Active?.FacetIndex ?? -1; + foreach (var facet in controller.Facets) + { + var isOpen = controller.IsOpen(facet.Index); + var label = facet.DimensionsKnown ? facet.Name : facet.Name + " (set size)"; + if (ImGui.MenuItem(label, "", facet.Index == activeFacet)) + { + if (!facet.DimensionsKnown || !facet.HasStatics) + { + if (AllWindows.TryGetValue(typeof(OptionsWindow), out var options)) + options.Show = true; + } + else + { + controller.OpenWorld(facet); + } + } + } + ImGui.EndMenu(); + } + private void DrawStatusBar() { if (ImGuiEx.BeginStatusBar()) diff --git a/CentrED/UI/Windows/MinimapWindow.cs b/CentrED/UI/Windows/MinimapWindow.cs index 87d3fcbf..904b08c7 100644 --- a/CentrED/UI/Windows/MinimapWindow.cs +++ b/CentrED/UI/Windows/MinimapWindow.cs @@ -22,13 +22,21 @@ public class MinimapWindow : Window protected override void InternalDraw() { - if (!CEDClient.Running) + var world = CEDGame.Worlds.Focused ?? CEDGame.Worlds.Active; + if (world == null || !world.Client.Running) { ImGui.Text(LangManager.Get(NOT_CONNECTED)); return; } + var radar = world.RadarMap; + if (radar?.Texture == null) + { + ImGui.Text(LangManager.Get(NOT_CONNECTED)); + return; + } + var map = world.Map; ImGui.Text(LangManager.Get(FAVORITES)); - if (ImGui.BeginChild("Favorites", new Vector2(RadarMap.Instance.Texture.Width, 100))) + if (ImGui.BeginChild("Favorites", new Vector2(radar.Texture.Width, 100))) { ImGui.InputText(LangManager.Get(NAME), ref _inputFavoriteName, 64); ImGui.SameLine(); @@ -41,8 +49,8 @@ protected override void InternalDraw() _inputFavoriteName, new() { - X = (ushort)CEDGame.MapManager.TilePosition.X, - Y = (ushort)CEDGame.MapManager.TilePosition.Y + X = (ushort)map.TilePosition.X, + Y = (ushort)map.TilePosition.Y } ); ProfileManager.Save(); @@ -56,7 +64,7 @@ protected override void InternalDraw() { ImGui.SameLine(); } - if (ImGui.GetCursorPos().X + 75 >= RadarMap.Instance.Texture.Width) + if (ImGui.GetCursorPos().X + 75 >= radar.Texture.Width) { ImGui.NewLine(); } @@ -66,7 +74,7 @@ protected override void InternalDraw() //tooltip for button what shows the key if (ImGui.Button($"{name}", new Vector2(75, 19))) { - CEDGame.MapManager.TilePosition = new Point(coords.X, coords.Y); + map.TilePosition = new Point(coords.X, coords.Y); } ImGuiEx.Tooltip($"X:{coords.X} Y:{coords.Y}"); @@ -114,13 +122,13 @@ protected override void InternalDraw() ImGui.PushItemWidth(100); if (ImGui.InputInt2("X/Y", ref mapPos[0])) { - CEDGame.MapManager.TilePosition = new Point(mapPos[0], mapPos[1]); + map.TilePosition = new Point(mapPos[0], mapPos[1]); }; ImGui.PopItemWidth(); if (ImGui.BeginChild("Minimap", Vector2.Zero, ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar)) { var currentPos = ImGui.GetCursorScreenPos(); - var tex = RadarMap.Instance.Texture; + var tex = radar.Texture; CEDGame.UIManager.DrawImage(tex, tex.Bounds); ImGui.SetCursorScreenPos(currentPos); @@ -133,7 +141,7 @@ protected override void InternalDraw() { if (ImGui.Button(LangManager.Get(REFRESH))) { - CEDClient.Send(new RequestRadarMapPacket()); + radar.Refresh(); ImGui.CloseCurrentPopup(); } ImGui.EndPopup(); @@ -144,18 +152,18 @@ protected override void InternalDraw() var newPos = (ImGui.GetMousePos() - currentPos) * 8; if (held) { - CEDGame.MapManager.TilePosition = new Point((int)newPos.X, (int)newPos.Y); + map.TilePosition = new Point((int)newPos.X, (int)newPos.Y); } mapPos[0] = (int)newPos.X; mapPos[1] = (int)newPos.Y; } else { - mapPos[0] = CEDGame.MapManager.TilePosition.X; - mapPos[1] = CEDGame.MapManager.TilePosition.Y; + mapPos[0] = map.TilePosition.X; + mapPos[1] = map.TilePosition.Y; } - var rect = CEDGame.MapManager.ViewRange; + var rect = map.ViewRange; var center = new Point(rect.X1 + rect.Width / 2, rect.Y1 + rect.Height / 2); var p1 = currentPos + new Vector2(rect.X1 / 8, center.Y / 8); var p2 = currentPos + new Vector2(center.X / 8, rect.Y1 / 8); diff --git a/CentrED/UI/Windows/OptionsWindow.cs b/CentrED/UI/Windows/OptionsWindow.cs index ce2fabbe..8e9dda15 100644 --- a/CentrED/UI/Windows/OptionsWindow.cs +++ b/CentrED/UI/Windows/OptionsWindow.cs @@ -1,4 +1,5 @@ using CentrED.Lights; +using CentrED.Map; using Hexa.NET.ImGui; using Microsoft.Xna.Framework.Input; using static CentrED.Application; @@ -18,6 +19,8 @@ public OptionsWindow(Keymap keymap) public override ImGuiWindowFlags WindowFlags => ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize; private int _lightLevel = 30; + private string _mapsFolder = Config.Instance.Facets.MapsFolder; + private int _basePort = Config.Instance.Facets.BasePort; private Vector4 _virtualLayerFillColor = new(0.2f, 0.2f, 0.2f, 0.1f); private Vector4 _virtualLayerBorderColor = new(1.0f, 1.0f, 1.0f, 1.0f); private Vector4 _terrainGridFlatColor = new(0.5f, 0.5f, 0.0f, 0.5f); @@ -126,10 +129,168 @@ protected override void InternalDraw() } ImGui.EndTabItem(); } + DrawFacetOptions(); ImGui.EndTabBar(); } } + private void DrawFacetOptions() + { + if (!ImGui.BeginTabItem("Facets")) + return; + + var settings = Config.Instance.Facets; + var controller = CEDGame.FacetController; + + ImGui.TextWrapped + ( + "Each mapX.mul in the maps folder becomes a world tab. Opening the first tab starts a local " + + "embedded server that hosts every listed map; each tab then renders on demand. Restart to " + + "pick up newly-added maps." + ); + ImGui.Separator(); + + if (ImGui.InputText("Maps folder", ref _mapsFolder, 1024)) + settings.MapsFolder = _mapsFolder; + ImGui.SameLine(); + if (ImGui.Button("...")) + { + var def = _mapsFolder.Length == 0 ? Environment.CurrentDirectory : _mapsFolder; + if (TinyFileDialogs.TrySelectFolder("Select Maps Folder", def, out var newPath)) + { + _mapsFolder = newPath; + settings.MapsFolder = newPath; + Config.Save(); + controller.Refresh(); + controller.RequestSync(); + } + } + + if (ImGui.InputInt("Base port", ref _basePort)) + settings.BasePort = _basePort; + + if (ImGui.Button("Rescan")) + { + Config.Save(); + controller.Refresh(); + controller.RequestSync(); + } + ImGui.SameLine(); + if (ImGui.Button("Save settings")) + { + Config.Save(); + controller.Refresh(); + controller.RequestSync(); + } + + if (controller.Status.Length > 0) + ImGui.TextColored(ImGuiColor.Pink, controller.Status); + + ImGui.Separator(); + var facets = controller.Facets.ToArray(); + if (facets.Length == 0) + { + ImGui.TextColored(ImGuiColor.Red, "No mapX.mul files found in the maps folder."); + ImGui.EndTabItem(); + return; + } + + if (ImGui.BeginTable("FacetsTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("Name"); + ImGui.TableSetupColumn("Width"); + ImGui.TableSetupColumn("Height"); + ImGui.TableSetupColumn("Port"); + ImGui.TableSetupColumn(""); + ImGui.TableHeadersRow(); + + foreach (var facet in facets) + { + ImGui.TableNextRow(); + var isOpen = controller.IsOpen(facet.Index); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + var name = facet.Name; + if (ImGui.InputText($"##name{facet.Index}", ref name, 64)) + SetFacetName(settings, facet, name); + if (ImGui.IsItemDeactivatedAfterEdit()) + Config.Save(); + if (!facet.DimensionsKnown) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColor.Pink, "(set size)"); + } + if (!facet.HasStatics) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColor.Red, "(no statics)"); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(80); + int w = facet.Width; + if (ImGui.InputInt($"##w{facet.Index}", ref w)) + SetFacetSize(settings, facet, w, facet.Height); + if (ImGui.IsItemDeactivatedAfterEdit()) + Config.Save(); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(80); + int h = facet.Height; + if (ImGui.InputInt($"##h{facet.Index}", ref h)) + SetFacetSize(settings, facet, facet.Width, h); + if (ImGui.IsItemDeactivatedAfterEdit()) + Config.Save(); + + ImGui.TableNextColumn(); + ImGui.Text(facet.Port.ToString()); + + ImGui.TableNextColumn(); + ImGui.BeginDisabled(controller.Switching || !facet.DimensionsKnown || !facet.HasStatics); + if (ImGui.Button($"{(isOpen ? "Focus" : "Open")}##{facet.Index}")) + controller.OpenWorld(facet); + ImGui.EndDisabled(); + if (controller.IsHosted(facet.Index)) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColor.Green, "hosted"); + } + } + ImGui.EndTable(); + } + ImGui.EndTabItem(); + } + + private static void SetFacetName(FacetSettings settings, Facet facet, string name) + { + facet.Name = name; + GetOrAddOverride(settings, facet).Name = name; + } + + private static void SetFacetSize(FacetSettings settings, Facet facet, int width, int height) + { + if (width > 0) + facet.Width = (ushort)Math.Min(width, ushort.MaxValue); + if (height > 0) + facet.Height = (ushort)Math.Min(height, ushort.MaxValue); + facet.DimensionsKnown = facet.Width > 0 && facet.Height > 0; + var ovr = GetOrAddOverride(settings, facet); + ovr.Width = facet.Width; + ovr.Height = facet.Height; + } + + private static FacetOverride GetOrAddOverride(FacetSettings settings, Facet facet) + { + var ovr = settings.Overrides.Find(o => o.Index == facet.Index); + if (ovr == null) + { + ovr = new FacetOverride { Index = facet.Index, Name = facet.Name }; + settings.Overrides.Add(ovr); + } + return ovr; + } + private string assigningActionName = ""; private byte assignedKeyNumber = 0; diff --git a/Client/CentrEDClient.cs b/Client/CentrEDClient.cs index 3f717a73..30b891eb 100644 --- a/Client/CentrEDClient.cs +++ b/Client/CentrEDClient.cs @@ -17,6 +17,7 @@ namespace CentrED.Client; public record struct User(string Username, AccessLevel AccessLevel, List Regions); public record struct Region(string Name, List Areas); public record struct Admin(List Users, List Regions); +public record struct ServerFacet(int Index, string Name, ushort Width, ushort Height); public enum ClientState { @@ -54,6 +55,16 @@ public sealed class CentrEDClient : ILogging internal TileDataLand[]? LandTileData; internal TileDataStatic[]? StaticTileData; public Admin Admin = new([], []); + public int Facet { get; private set; } + public List ServerFacets { get; } = new(); + public event Action? ServerFacetsChanged; + + internal void SetServerFacets(List facets) + { + ServerFacets.Clear(); + ServerFacets.AddRange(facets); + ServerFacetsChanged?.Invoke(); + } private void Reset() { @@ -71,6 +82,8 @@ private void Reset() RequestedBlocksQueue.Clear(); RequestedBlocks.Clear(); Clients.Clear(); + ServerFacets.Clear(); + Facet = 0; State = ClientState.Disconnected; ServerState = ServerState.Running; Status = ""; @@ -86,12 +99,13 @@ private void RegisterPacketHandlers(NetState ns) ns.RegisterPacketHandler(0x0D, 0, RadarMap.OnRadarHandlerPacket); } - public void Connect(string hostname, int port, string username, string password) + public void Connect(string hostname, int port, string username, string password, int facet = 0) { Reset(); Hostname = hostname; Port = port; Password = password; + Facet = facet; var ipAddress = Dns.GetHostAddresses(hostname)[0]; var ipEndPoint = new IPEndPoint(ipAddress, port); var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); @@ -100,6 +114,8 @@ public void Connect(string hostname, int port, string username, string password) NetState = new NetState(this, socket, recvPipeSize: RecvPipeSize); RegisterPacketHandlers(NetState); NetState.Username = username; + if (facet > 0) + NetState.Send(new SelectFacetPacket(facet)); NetState.Send(new LoginRequestPacket(username, password)); NetState.Flush(); diff --git a/Client/ConnectionHandling.cs b/Client/ConnectionHandling.cs index 02937672..ca3fb613 100644 --- a/Client/ConnectionHandling.cs +++ b/Client/ConnectionHandling.cs @@ -15,6 +15,23 @@ static ConnectionHandling() Handlers[0x03] = new PacketHandler(0, OnLoginResponsePacket); Handlers[0x04] = new PacketHandler(0, OnServerStatePacket); Handlers[0x05] = new PacketHandler(0, OnQuitAckPacket); + Handlers[0x22] = new PacketHandler(0, OnFacetListPacket); + } + + private static void OnFacetListPacket(SpanReader reader, NetState ns) + { + ns.LogDebug("Client OnFacetListPacket"); + var count = reader.ReadByte(); + var facets = new List(count); + for (var i = 0; i < count; i++) + { + var index = reader.ReadByte(); + var width = reader.ReadUInt16(); + var height = reader.ReadUInt16(); + var name = reader.ReadString(); + facets.Add(new ServerFacet(index, name, width, height)); + } + ns.Parent.SetServerFacets(facets); } public static void OnConnectionHandlerPacket(SpanReader reader, NetState ns) diff --git a/Client/Packets.cs b/Client/Packets.cs index 486177ce..500c65a9 100644 --- a/Client/Packets.cs +++ b/Client/Packets.cs @@ -23,6 +23,23 @@ public QuitPacket() : base(0x02, 0) } } +public class SelectFacetPacket : Packet +{ + public SelectFacetPacket(int index) : base(0x02, 0) + { + Writer.Write((byte)0x20); + Writer.Write((byte)index); + } +} + +public class RequestFacetListPacket : Packet +{ + public RequestFacetListPacket() : base(0x02, 0) + { + Writer.Write((byte)0x21); + } +} + public class RequestBlocksPacket : Packet { public RequestBlocksPacket(PointU16 coord) : base(0x04, 0) diff --git a/Server/AdminHandling.cs b/Server/AdminHandling.cs index 8238424d..d93b8d05 100644 --- a/Server/AdminHandling.cs +++ b/Server/AdminHandling.cs @@ -38,8 +38,7 @@ public static void OnAdminHandlerPacket(SpanReader reader, NetState n private static void OnFlushPacket(SpanReader reader, NetState ns) { ns.LogDebug("Server OnFlushPacket"); - ns.Parent.Landscape.Flush(); - ns.Parent.Config.Flush(); + ns.Parent.Save(); } private static void OnShutdownPacket(SpanReader reader, NetState ns) diff --git a/Server/CEDServer.cs b/Server/CEDServer.cs index 8a1b3729..ae36daf4 100644 --- a/Server/CEDServer.cs +++ b/Server/CEDServer.cs @@ -17,9 +17,10 @@ public class CEDServer : ILogging, IDisposable private ProtocolVersion ProtocolVersion; private Socket Listener { get; } = null!; public ConfigRoot Config { get; } - public ServerLandscape Landscape { get; } + public List Landscapes { get; } = new(); + public ServerLandscape Landscape => Landscapes[0]; public HashSet> Clients { get; } = new(8); - private readonly Dictionary>> _blockSubscriptions = new(); + private readonly Dictionary, ServerLandscape> _clientLandscape = new(); private readonly ConcurrentQueue> _connectedQueue = new(); private readonly Queue> _toDispose = new(); @@ -47,8 +48,23 @@ public CEDServer(ConfigRoot config, TextWriter? logOutput = default) ProtocolVersion = Config.CentrEdPlus ? ProtocolVersion.CentrEDPlus : ProtocolVersion.CentrED; LogInfo("Running as " + (Config.CentrEdPlus ? "CentrED+ 0.7.9" : "CentrED 0.6.3")); Console.CancelKeyPress += ConsoleOnCancelKeyPress; - Landscape = new ServerLandscape(config, _logger); - Listener = Bind(new IPEndPoint(IPAddress.Any, Config.Port)); + var facets = config.EffectiveFacets; + for (var i = 0; i < facets.Count; i++) + { + var name = string.IsNullOrEmpty(facets[i].Name) ? facets[i].MapPath : facets[i].Name; + LogInfo($"Loading facet {i}: {name}"); + try + { + Landscapes.Add(new ServerLandscape(config, facets[i], i, _logger)); + } + catch (Exception e) + { + LogError($"Failed to load facet {i} ({name}): {e.Message}. Skipping it."); + } + } + if (Landscapes.Count == 0) + throw new InvalidOperationException("No facets could be loaded."); + Listener = Bind(new IPEndPoint(Config.BindAddress, Config.Port)); LogInfo("Initialization done"); } @@ -72,6 +88,23 @@ public CEDServer(ConfigRoot config, TextWriter? logOutput = default) return Config.Regions.Find(a => a.Name == name); } + public ServerLandscape GetLandscape(NetState ns) => + _clientLandscape.TryGetValue(ns, out var landscape) ? landscape : Landscapes[0]; + + public int GetFacetIndex(NetState ns) => GetLandscape(ns).FacetIndex; + + public bool IsFacetSelected(NetState ns) => _clientLandscape.ContainsKey(ns); + + public bool BindFacet(NetState ns, int index) + { + var landscape = Landscapes.Find(l => l.FacetIndex == index); + if (landscape == null) + return false; + _clientLandscape[ns] = landscape; + landscape.RegisterPacketHandlers(ns); + return true; + } + private Socket Bind(IPEndPoint endPoint) { var s = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) @@ -144,7 +177,6 @@ private async void Listen() ProtocolVersion = ProtocolVersion }; RegisterPacketHandlers(ns); - Landscape.RegisterPacketHandlers(ns); _connectedQueue.Enqueue(ns); } } @@ -224,6 +256,7 @@ private void ProcessNetStates() while (_toDispose.TryDequeue(out var ns)) { Clients.Remove(ns); + _clientLandscape.Remove(ns); if (ns.Username != "") { Broadcast(new ClientDisconnectedPacket(ns)); @@ -246,7 +279,8 @@ private void AutoSave() public void Save() { - Landscape.Flush(); + foreach (var landscape in Landscapes) + landscape.Flush(); Config.Flush(); _lastFlush = DateTime.Now; } @@ -281,24 +315,10 @@ public void Broadcast(Packet packet) } } - public HashSet> GetBlockSubscriptions(ushort x, ushort y) - { - Landscape.AssertBlockCoords(x, y); - var key = Landscape.GetBlockNumber(x, y); - - if (!_blockSubscriptions.TryGetValue(key, out var subscriptions)) - { - subscriptions = []; - _blockSubscriptions.Add(key, subscriptions); - } - - subscriptions.RemoveWhere(ns => !ns.Running); - return subscriptions; - } - private void Backup() { - Landscape.Flush(); + foreach (var landscape in Landscapes) + landscape.Flush(); var logMsg = "Automatic backup in progress"; LogInfo(logMsg); Broadcast(new ServerStatePacket(ServerState.Other, logMsg)); @@ -314,7 +334,8 @@ private void Backup() } backupDir = $"{Config.AutoBackup.Directory}/Backup1"; - Landscape.Backup(backupDir); + foreach (var landscape in Landscapes) + landscape.Backup(backupDir); Broadcast(new ServerStatePacket(ServerState.Running)); LogInfo("Automatic backup finished."); @@ -351,17 +372,20 @@ private void ProcessCommands() { case ["save"]: Console.Write("Saving..."); - Landscape.Flush(); + foreach (var landscape in Landscapes) + landscape.Flush(); Console.WriteLine("Done"); break; case ["save", string dir]: Console.Write($"Saving to {dir}..."); - Landscape.Backup(dir); + foreach (var landscape in Landscapes) + landscape.Backup(dir); Console.WriteLine("Done"); break; case ["supersave"]: Console.Write("Supersaving..."); - Landscape.SuperSave(); + foreach (var landscape in Landscapes) + landscape.SuperSave(); Console.WriteLine("Done"); break; default: PrintHelp(); break; @@ -387,7 +411,8 @@ private void PrintHelp() public void Dispose() { Listener.Dispose(); - Landscape.Dispose(); + foreach (var landscape in Landscapes) + landscape.Dispose(); } public void LogInfo(string message) diff --git a/Server/Config/ConfigRoot.cs b/Server/Config/ConfigRoot.cs index 3c93e83b..b75cd6b3 100644 --- a/Server/Config/ConfigRoot.cs +++ b/Server/Config/ConfigRoot.cs @@ -1,4 +1,5 @@ -using System.Xml; +using System.Net; +using System.Xml; namespace CentrED.Server.Config; @@ -12,6 +13,8 @@ public class ConfigRoot public bool CentrEdPlus { get; set; } public int Port { get; set; } = 2597; public Map Map { get; set; } = new(); + public List Facets { get; set; } = new(); + public IReadOnlyList EffectiveFacets => Facets.Count > 0 ? Facets : new[] { Map }; public string Tiledata { get; set; } = "tiledata.mul"; public string Radarcol { get; set; } = "radarcol.mul"; public string Hues { get; set; } = "hues.mul"; @@ -22,6 +25,8 @@ public class ConfigRoot public bool Changed { get; set; } public string FilePath { get; set; } = DefaultPath; + public IPAddress BindAddress { get; set; } = IPAddress.Any; + public void Invalidate() { Changed = true; @@ -194,7 +199,14 @@ internal void Write(XmlWriter writer) writer.WriteElementString("CentrEdPlus", XmlConvert.ToString(CentrEdPlus)); writer.WriteElementString("Port", XmlConvert.ToString(Port)); - Map.Write(writer); + EffectiveFacets[0].Write(writer); + if (Facets.Count > 0) + { + writer.WriteStartElement("Facets"); + foreach (var facet in Facets) + facet.Write(writer, "Facet"); + writer.WriteEndElement(); + } writer.WriteElementString("Tiledata", Tiledata); writer.WriteElementString("Radarcol", Radarcol); writer.WriteElementString("Hues", Hues); @@ -242,6 +254,18 @@ internal static ConfigRoot Read(XmlReader reader) result.Map = Map.Read(reader); break; + case "Facets": + using (var facetsReader = reader.ReadSubtree()) + { + facetsReader.Read(); + while (facetsReader.Read()) + { + if (facetsReader.NodeType == XmlNodeType.Element && facetsReader.Name == "Facet") + result.Facets.Add(Map.Read(facetsReader)); + } + } + break; + case "Tiledata": result.Tiledata = reader.ReadElementContentAsString(); break; diff --git a/Server/Config/Map.cs b/Server/Config/Map.cs index 5433733c..3b5682fa 100644 --- a/Server/Config/Map.cs +++ b/Server/Config/Map.cs @@ -4,15 +4,18 @@ namespace CentrED.Server.Config; public class Map { + public string Name { get; set; } = ""; public string MapPath { get; set; } = "map0.mul"; public string StaIdx { get; set; } = "staidx0.mul"; public string Statics { get; set; } = "statics0.mul"; public ushort Width { get; set; } = 896; public ushort Height { get; set; } = 512; - internal void Write(XmlWriter writer) + internal void Write(XmlWriter writer, string element = "Map") { - writer.WriteStartElement("Map"); + writer.WriteStartElement(element); + if (!string.IsNullOrEmpty(Name)) + writer.WriteElementString("Name", Name); writer.WriteElementString("Map", MapPath); writer.WriteElementString("StaIdx", StaIdx); writer.WriteElementString("Statics", Statics); @@ -32,6 +35,9 @@ internal static Map Read(XmlReader reader) { switch (sub.Name) { + case "Name": + result.Name = sub.ReadElementContentAsString(); + break; case "Map": result.MapPath = sub.ReadElementContentAsString(); break; diff --git a/Server/ConnectionHandling.cs b/Server/ConnectionHandling.cs index a841f0ef..558e44d0 100644 --- a/Server/ConnectionHandling.cs +++ b/Server/ConnectionHandling.cs @@ -13,6 +13,20 @@ static ConnectionHandling() Handlers[0x03] = new PacketHandler(0, OnLoginRequestPacket); Handlers[0x05] = new PacketHandler(0, OnQuitPacket); + Handlers[0x20] = new PacketHandler(0, OnSelectFacetPacket); + Handlers[0x21] = new PacketHandler(0, OnRequestFacetListPacket); + } + + private static void OnSelectFacetPacket(SpanReader reader, NetState ns) + { + var index = reader.ReadByte(); + if (!ns.Parent.BindFacet(ns, index)) + ns.LogDebug($"Client requested invalid facet index {index}"); + } + + private static void OnRequestFacetListPacket(SpanReader reader, NetState ns) + { + ns.Send(new FacetListPacket(ns.Parent)); } public static void OnConnectionHandlerPacket(SpanReader reader, NetState ns) @@ -28,6 +42,8 @@ private static void OnLoginRequestPacket(SpanReader reader, NetState ns.LogDebug("Server OnLoginRequestPacket"); var username = reader.ReadString(); var password = reader.ReadString(); + if (!ns.Parent.IsFacetSelected(ns)) + ns.Parent.BindFacet(ns, 0); var account = ns.Parent.GetAccount(username); if (account == null) { @@ -47,16 +63,18 @@ private static void OnLoginRequestPacket(SpanReader reader, NetState ns.Send(new LoginResponsePacket(LoginState.InvalidPassword)); ns.Disconnect(); } - else if (ns.Parent.Clients.Any(client => client.Username == account.Name)) + else if (ns.Parent.Clients.Any(client => + client.Username == account.Name && ns.Parent.GetLandscape(client) == ns.Parent.GetLandscape(ns))) { ns.Send(new LoginResponsePacket(LoginState.AlreadyLoggedIn)); ns.Disconnect(); } else { - ns.LogInfo($"Login {username}"); + ns.LogInfo($"Login {username} (facet {ns.Parent.GetFacetIndex(ns)})"); ns.Username = account.Name; ns.Send(new LoginResponsePacket(LoginState.Ok, ns)); + ns.Send(new FacetListPacket(ns.Parent)); ns.SendCompressed(new ClientListPacket(ns)); ns.Parent.Broadcast(new ClientConnectedPacket(ns)); ns.Send(new SetClientPosPacket(ns)); diff --git a/Server/Map/ServerLandscape.cs b/Server/Map/ServerLandscape.cs index 14698c19..b7e23248 100644 --- a/Server/Map/ServerLandscape.cs +++ b/Server/Map/ServerLandscape.cs @@ -9,21 +9,43 @@ public sealed partial class ServerLandscape : BaseLandscape, IDisposable, ILoggi { private readonly Logger _logger; + public string Name { get; } + public int FacetIndex { get; } + + private readonly Dictionary>> _blockSubscriptions = new(); + + public HashSet> GetBlockSubscriptions(ushort x, ushort y) + { + AssertBlockCoords(x, y); + var key = GetBlockNumber(x, y); + if (!_blockSubscriptions.TryGetValue(key, out var subscriptions)) + { + subscriptions = []; + _blockSubscriptions.Add(key, subscriptions); + } + subscriptions.RemoveWhere(ns => !ns.Running); + return subscriptions; + } + public ServerLandscape ( ConfigRoot config, + Config.Map facet, + int facetIndex, Logger logger - ) : base(config.Map.Width, config.Map.Height) + ) : base(facet.Width, facet.Height) { _logger = logger; - var mapFile = new FileInfo(config.Map.MapPath); + FacetIndex = facetIndex; + Name = string.IsNullOrEmpty(facet.Name) ? $"Facet {facetIndex}" : facet.Name; + var mapFile = new FileInfo(facet.MapPath); if (!mapFile.Exists) { Console.WriteLine("Map file not found, do you want to create it? [y/n]"); if (Console.ReadLine() == "y") { InitMap(mapFile); - mapFile = new FileInfo(config.Map.MapPath); + mapFile = new FileInfo(facet.MapPath); } } if (mapFile.IsReadOnly) @@ -41,16 +63,16 @@ Logger logger } _logger.LogInfo($"Loaded {_map.Name}"); - var staidxFile = new FileInfo(config.Map.StaIdx); - var staticsFile = new FileInfo(config.Map.Statics); + var staidxFile = new FileInfo(facet.StaIdx); + var staticsFile = new FileInfo(facet.Statics); if (!staidxFile.Exists && !staticsFile.Exists) { Console.WriteLine("Statics files not found, do you want to create them? [y/n]"); if (Console.ReadLine() == "y") { InitStatics(staticsFile, staidxFile); - staidxFile = new FileInfo(config.Map.StaIdx); - staticsFile = new FileInfo(config.Map.Statics); + staidxFile = new FileInfo(facet.StaIdx); + staticsFile = new FileInfo(facet.Statics); } } if(!staidxFile.Exists) @@ -96,7 +118,7 @@ Logger logger BlockUnloaded += OnRemovedCachedObject; //Cache entire strip of chunks to reduce IO in case someone is doing naive iteration over entire map - BlockCache.Resize(Math.Max(config.Map.Width, config.Map.Height) + 1); + BlockCache.Resize(Math.Max(facet.Width, facet.Height) + 1); } private void InitMap(FileInfo map) diff --git a/Server/Map/ServerLandscapePacketHandlers.cs b/Server/Map/ServerLandscapePacketHandlers.cs index 4d07125f..c7350670 100644 --- a/Server/Map/ServerLandscapePacketHandlers.cs +++ b/Server/Map/ServerLandscapePacketHandlers.cs @@ -55,7 +55,7 @@ private void OnRequestBlocksPacket(SpanReader reader, NetState ns) } foreach (var coord in coords) { - var subscriptions = ns.Parent.GetBlockSubscriptions(coord.X, coord.Y); + var subscriptions = GetBlockSubscriptions(coord.X, coord.Y); subscriptions.Add(ns); } } @@ -67,7 +67,7 @@ private void OnFreeBlockPacket(SpanReader reader, NetState ns) return; var x = reader.ReadUInt16(); var y = reader.ReadUInt16(); - var subscriptions = ns.Parent.GetBlockSubscriptions(x, y); + var subscriptions = GetBlockSubscriptions(x, y); subscriptions.Remove(ns); } private void OnDrawMapPacket(SpanReader reader, NetState ns) @@ -88,7 +88,7 @@ private void OnDrawMapPacket(SpanReader reader, NetState ns) LandBlock block = tile.Block!; var packet = new DrawMapPacket(tile); - foreach (var netState in ns.Parent.GetBlockSubscriptions(block.X, block.Y)) + foreach (var netState in GetBlockSubscriptions(block.X, block.Y)) { netState.Send(packet); } @@ -113,7 +113,7 @@ private void OnInsertStaticPacket(SpanReader reader, NetState ns) block.SortTiles(ref TileDataProvider.StaticTiles); var packet = new InsertStaticPacket(tile); - foreach (var netState in ns.Parent.GetBlockSubscriptions(block.X, block.Y)) + foreach (var netState in GetBlockSubscriptions(block.X, block.Y)) { netState.Send(packet); } @@ -138,7 +138,7 @@ private void OnDeleteStaticPacket(SpanReader reader, NetState ns) InternalRemoveStatic(block, tile); var packet = new DeleteStaticPacket(tile); - foreach (var netState in ns.Parent.GetBlockSubscriptions(block.X, block.Y)) + foreach (var netState in GetBlockSubscriptions(block.X, block.Y)) { netState.Send(packet); } @@ -165,7 +165,7 @@ private void OnElevateStaticPacket(SpanReader reader, NetState ns) InternalSetStaticZ(tile, newZ); block.SortTiles(ref TileDataProvider.StaticTiles); - foreach (var netState in ns.Parent.GetBlockSubscriptions(block.X, block.Y)) + foreach (var netState in GetBlockSubscriptions(block.X, block.Y)) { netState.Send(packet); } @@ -214,8 +214,8 @@ private void OnMoveStaticPacket(SpanReader reader, NetState ns) targetBlock.SortTiles(ref TileDataProvider.StaticTiles); - var sourceSubscriptions = ns.Parent.GetBlockSubscriptions(sourceBlock.X, sourceBlock.Y); - var targetSubscriptions = ns.Parent.GetBlockSubscriptions(targetBlock.X, targetBlock.Y); + var sourceSubscriptions = GetBlockSubscriptions(sourceBlock.X, sourceBlock.Y); + var targetSubscriptions = GetBlockSubscriptions(targetBlock.X, targetBlock.Y); var moveSubscriptions = sourceSubscriptions.Intersect(targetSubscriptions); var deleteSubscriptions = sourceSubscriptions.Except(targetSubscriptions); @@ -256,7 +256,7 @@ private void OnHueStaticPacket(SpanReader reader, NetState ns) var packet = new HueStaticPacket(tile, newHue); InternalSetStaticHue(tile, newHue); - foreach (var netState in ns.Parent.GetBlockSubscriptions(block.X, block.Y)) + foreach (var netState in GetBlockSubscriptions(block.X, block.Y)) { netState.Send(packet); } @@ -375,7 +375,7 @@ private void OnLargeScaleCommandPacket(SpanReader reader, NetState ns } //Notify affected clients - foreach (var netState in ns.Parent.GetBlockSubscriptions(blockX, blockY)) + foreach (var netState in GetBlockSubscriptions(blockX, blockY)) { clients[netState].Add(new PointU16(blockX, blockY)); } @@ -388,7 +388,7 @@ private void OnLargeScaleCommandPacket(SpanReader reader, NetState ns if(affectedBlocks[blockId]) continue; - foreach (var netState in ns.Parent.GetBlockSubscriptions(blockX, blockY)!) + foreach (var netState in GetBlockSubscriptions(blockX, blockY)!) { clients[netState].Add(new PointU16(blockX, blockY)); } diff --git a/Server/Packets.cs b/Server/Packets.cs index 1f09e1f7..1a683ef7 100644 --- a/Server/Packets.cs +++ b/Server/Packets.cs @@ -8,15 +8,16 @@ class BlockPacket : Packet { public BlockPacket(IEnumerable coords, NetState ns) : base(0x04, 0) { + var landscape = ns.Landscape(); foreach (var coord in coords) { - var mapBlock = ns.Parent.Landscape.GetLandBlock(coord.X, coord.Y); - var staticsBlock = ns.Parent.Landscape.GetStaticBlock(coord.X, coord.Y); + var mapBlock = landscape.GetLandBlock(coord.X, coord.Y); + var staticsBlock = landscape.GetStaticBlock(coord.X, coord.Y); coord.Write(Writer); mapBlock.Write(Writer); Writer.Write((ushort)staticsBlock.TotalTilesCount); - staticsBlock.SortTiles(ref ns.Parent.Landscape.TileDataProvider.StaticTiles); + staticsBlock.SortTiles(ref landscape.TileDataProvider.StaticTiles); staticsBlock.Write(Writer); } } @@ -106,6 +107,23 @@ public ProtocolVersionPacket(uint version) : base(0x02, 0) } } +public class FacetListPacket : Packet +{ + public FacetListPacket(CEDServer server) : base(0x02, 0) + { + Writer.Write((byte)0x22); + var landscapes = server.Landscapes; + Writer.Write((byte)landscapes.Count); + foreach (var landscape in landscapes) + { + Writer.Write((byte)landscape.FacetIndex); + Writer.Write(landscape.Width); + Writer.Write(landscape.Height); + Writer.WriteStringNull(landscape.Name); + } + } +} + public class LoginResponsePacket : Packet { public LoginResponsePacket(LoginState state, NetState? ns = null) : base(0x02, 0) @@ -118,14 +136,15 @@ public LoginResponsePacket(LoginState state, NetState? ns = null) : b Writer.Write((byte)ns.AccessLevel()); if (ns.ProtocolVersion == ProtocolVersion.CentrEDPlus) Writer.Write((uint)Math.Abs((DateTime.Now - ns.Parent.StartTime).TotalSeconds)); - Writer.Write(ns.Parent.Landscape.Width); - Writer.Write(ns.Parent.Landscape.Height); + var landscape = ns.Landscape(); + Writer.Write(landscape.Width); + Writer.Write(landscape.Height); if (ns.ProtocolVersion == ProtocolVersion.CentrEDPlus) { uint flags = 0xF0000000; - if (ns.Parent.Landscape.TileDataProvider.Version == TileDataVersion.HighSeas) + if (landscape.TileDataProvider.Version == TileDataVersion.HighSeas) flags |= 0x8; - if (ns.Parent.Landscape.IsUop) + if (landscape.IsUop) flags |= 0x10; Writer.Write(flags); @@ -202,8 +221,8 @@ public class SetClientPosPacket : Packet public SetClientPosPacket(NetState ns) : base(0x0C, 0) { Writer.Write((byte)0x04); - Writer.Write((ushort)Math.Clamp(ns.Account().LastPos.X, 0, ns.Parent.Landscape.WidthInTiles - 1)); - Writer.Write((ushort)Math.Clamp(ns.Account().LastPos.Y, 0, ns.Parent.Landscape.HeightInTiles - 1)); + Writer.Write((ushort)Math.Clamp(ns.Account().LastPos.X, 0, ns.Landscape().WidthInTiles - 1)); + Writer.Write((ushort)Math.Clamp(ns.Account().LastPos.Y, 0, ns.Landscape().HeightInTiles - 1)); } } diff --git a/Server/ServerNetState.cs b/Server/ServerNetState.cs index e7f1c71f..4fbd337b 100644 --- a/Server/ServerNetState.cs +++ b/Server/ServerNetState.cs @@ -1,5 +1,6 @@ using CentrED.Network; using CentrED.Server.Config; +using CentrED.Server.Map; namespace CentrED.Server; @@ -9,6 +10,11 @@ public static bool ValidateAccess(this NetState ns, AccessLevel acces { return ns.AccessLevel() >= accessLevel; } + + public static ServerLandscape Landscape(this NetState ns) + { + return ns.Parent.GetLandscape(ns); + } public static Account Account(this NetState ns) {