From 238b5b49a743b5233844c2babc28c9e17c2a9451 Mon Sep 17 00:00:00 2001 From: ApacheTech Solutions Date: Mon, 30 Jun 2025 18:26:12 +0100 Subject: [PATCH] Fixed: Added thread safety to map layer generation. Fixed: Client and Server sharing the same level finalisation handler in world map manager. Refactor: Added code separation to world map manager level finalisation. Dev: Added XML Documentation to POCO classes. Dev: Added `.vs` folder to gitignore. --- .gitignore | 3 +- Systems/WorldMap/MapLayerData.cs | 18 ++ Systems/WorldMap/MapLayerUpdate.cs | 13 + Systems/WorldMap/OnMapToggle.cs | 13 + Systems/WorldMap/OnViewChangedPacket.cs | 28 +++ Systems/WorldMap/WorldMapManager.cs | 300 ++++++++++++------------ 6 files changed, 229 insertions(+), 146 deletions(-) create mode 100644 Systems/WorldMap/MapLayerData.cs create mode 100644 Systems/WorldMap/MapLayerUpdate.cs create mode 100644 Systems/WorldMap/OnMapToggle.cs create mode 100644 Systems/WorldMap/OnViewChangedPacket.cs diff --git a/.gitignore b/.gitignore index 2e9693e..634fe55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ obj -bin \ No newline at end of file +bin +/.vs \ No newline at end of file diff --git a/Systems/WorldMap/MapLayerData.cs b/Systems/WorldMap/MapLayerData.cs new file mode 100644 index 0000000..4118489 --- /dev/null +++ b/Systems/WorldMap/MapLayerData.cs @@ -0,0 +1,18 @@ +namespace Vintagestory.GameContent; + +/// +/// Represents a single map layer and its associated serialised data, sent to or from the client. +/// +[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] +public class MapLayerData +{ + /// + /// The identifier or key corresponding to the map layer this data belongs to. + /// + public string ForMapLayer { get; set; } + + /// + /// The raw, serialised binary data representing the layer's content or state. + /// + public byte[] Data { get; set; } +} \ No newline at end of file diff --git a/Systems/WorldMap/MapLayerUpdate.cs b/Systems/WorldMap/MapLayerUpdate.cs new file mode 100644 index 0000000..97321b7 --- /dev/null +++ b/Systems/WorldMap/MapLayerUpdate.cs @@ -0,0 +1,13 @@ +namespace Vintagestory.GameContent; + +/// +/// Represents a payload used to update the visible map layers for the current session. +/// +[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] +public class MapLayerUpdate +{ + /// + /// The collection of map layers that should be updated or applied. + /// + public MapLayerData[] Maplayers { get; set; } +} \ No newline at end of file diff --git a/Systems/WorldMap/OnMapToggle.cs b/Systems/WorldMap/OnMapToggle.cs new file mode 100644 index 0000000..cd0dacc --- /dev/null +++ b/Systems/WorldMap/OnMapToggle.cs @@ -0,0 +1,13 @@ +namespace Vintagestory.GameContent; + +/// +/// Represents a network packet indicating that the map should be opened or closed. +/// +[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] +public class OnMapToggle +{ + /// + /// Whether to open (true) or close (false) the map. + /// + public bool OpenOrClose { get; set; } +} \ No newline at end of file diff --git a/Systems/WorldMap/OnViewChangedPacket.cs b/Systems/WorldMap/OnViewChangedPacket.cs new file mode 100644 index 0000000..d376da3 --- /dev/null +++ b/Systems/WorldMap/OnViewChangedPacket.cs @@ -0,0 +1,28 @@ +namespace Vintagestory.GameContent; + +/// +/// Represents a network packet indicating that the client's view bounds have changed. +/// +[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] +public class OnViewChangedPacket +{ + /// + /// The minimum X coordinate of the view bounds. + /// + public int X1 { get; set; } + + /// + /// The minimum Z coordinate of the view bounds. + /// + public int Z1 { get; set; } + + /// + /// The maximum X coordinate of the view bounds. + /// + public int X2 { get; set; } + + /// + /// The maximum Z coordinate of the view bounds. + /// + public int Z2 { get; set; } +} \ No newline at end of file diff --git a/Systems/WorldMap/WorldMapManager.cs b/Systems/WorldMap/WorldMapManager.cs index 044657c..a4b5519 100644 --- a/Systems/WorldMap/WorldMapManager.cs +++ b/Systems/WorldMap/WorldMapManager.cs @@ -12,58 +12,30 @@ #nullable disable -namespace Vintagestory.GameContent -{ - [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] - public class MapLayerUpdate - { - public MapLayerData[] Maplayers; - } - - [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] - public class MapLayerData - { - public string ForMapLayer; - public byte[] Data; - } - - [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] - public class OnMapToggle - { - public bool OpenOrClose; - } - - [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)] - public class OnViewChangedPacket - { - public int X1; - public int Z1; - public int X2; - public int Z2; - } +namespace Vintagestory.GameContent; public class WorldMapManager : ModSystem, IWorldMapManager { - public Dictionary MapLayerRegistry = new Dictionary(); - public Dictionary LayerGroupPositions = new Dictionary(); + public Dictionary MapLayerRegistry { get; } = new Dictionary(); + public Dictionary LayerGroupPositions { get; } = new Dictionary(); + public bool IsShuttingDown { get; set; } - ICoreAPI api; + private ICoreAPI _api; // Client side stuff - ICoreClientAPI capi; - IClientNetworkChannel clientChannel; - public GuiDialogWorldMap worldMapDlg; + private ICoreClientAPI _capi; + private IClientNetworkChannel _clientChannel; + public GuiDialogWorldMap worldMapDlg { get; set; } public bool IsOpened => worldMapDlg?.IsOpened() == true; - // Client and Server side stuff - public List MapLayers = new List(); - Thread mapLayerGenThread; - public bool IsShuttingDown { get; set; } - + public List MapLayers { get; set; } = new List(); + private Thread _mapLayerGenThread; + private readonly object _mapLayerThreadLock = new(); + private const float _tickInterval = 20 / 1000f; + // Server side stuff - IServerNetworkChannel serverChannel; - + private IServerNetworkChannel _serverChannel; public override bool ShouldLoad(EnumAppSide side) { @@ -74,7 +46,7 @@ public override void Start(ICoreAPI api) { base.Start(api); RegisterDefaultMapLayers(); - this.api = api; + _api = api; } public void RegisterDefaultMapLayers() @@ -91,41 +63,69 @@ public void RegisterMapLayer(string code, double position) where T : MapLayer LayerGroupPositions[code] = position; } - #region Client side - - public override void StartClientSide(ICoreClientAPI api) + private void CreateMapLayerGenerationThread() { - base.StartClientSide(api); - - capi = api; - capi.Event.LevelFinalize += OnLvlFinalize; - - capi.Event.RegisterGameTickListener(OnClientTick, 20); + if (_mapLayerGenThread is { IsAlive: true }) return; + _mapLayerGenThread = new Thread(new ThreadStart(() => + { + while (!IsShuttingDown) + { + lock (_mapLayerThreadLock) + { + var mapLayersSnapshot = MapLayers.ToList(); + foreach (var layer in mapLayersSnapshot) + { + layer.OnOffThreadTick(_tickInterval); + } + } - capi.Settings.AddWatcher("showMinimapHud", (on) => { - ToggleMap(EnumDialogType.HUD); - }); + Thread.Sleep(20); + } + })) + { + IsBackground = true + }; + _mapLayerGenThread.Start(); + } - capi.Event.LeaveWorld += () => + private void LoadMapLayersFromRegistry() + { + lock (_mapLayerThreadLock) { - IsShuttingDown = true; - int i = 0; - while (mapLayerGenThread != null && mapLayerGenThread.IsAlive && i < 20) + MapLayers.Clear(); + + var mapLayerRegistrySnapshot = MapLayerRegistry.ToDictionary(k => k.Key, v => v.Value); + foreach (var val in mapLayerRegistrySnapshot) { - Thread.Sleep(50); - i++; + if (val.Key == "entities" && !_api.World.Config.GetAsBool("entityMapLayer")) continue; + var instance = (MapLayer)Activator.CreateInstance(val.Value, _api, this); + MapLayers.Add(instance); } - worldMapDlg?.Dispose(); - - foreach (var layer in MapLayers) + var mapLayersSnapshot = MapLayers.ToList(); + foreach (var layer in mapLayersSnapshot) { - layer?.OnShutDown(); - layer?.Dispose(); + layer.OnLoaded(); } - }; + } + } - clientChannel = + #region Client side + + public override void StartClientSide(ICoreClientAPI api) + { + base.StartClientSide(api); + _capi = api; + _capi.Event.LevelFinalize += OnLevelFinaliseClient; + _capi.Event.RegisterGameTickListener(OnClientTick, 20); + _capi.Settings.AddWatcher("showMinimapHud", (on) => + { + ToggleMap(EnumDialogType.HUD); + }); + + _capi.Event.LeaveWorld += OnPlayerLeaveWorld; + + _clientChannel = api.Network.RegisterChannel("worldmap") .RegisterMessageType(typeof(MapLayerUpdate)) .RegisterMessageType(typeof(OnViewChangedPacket)) @@ -134,8 +134,26 @@ public override void StartClientSide(ICoreClientAPI api) ; } + private void OnPlayerLeaveWorld() + { + IsShuttingDown = true; + int i = 0; + while (_mapLayerGenThread != null && _mapLayerGenThread.IsAlive && i < 20) + { + Thread.Sleep(50); + i++; + } + + worldMapDlg?.Dispose(); - private void onWorldMapLinkClicked(LinkTextComponent linkcomp) + foreach (var layer in MapLayers) + { + layer?.OnShutDown(); + layer?.Dispose(); + } + } + + private void OnWorldMapLinkClicked(LinkTextComponent linkcomp) { string[] xyzstr = linkcomp.Href.Substring("worldmap://".Length).Split('='); int x = xyzstr[1].ToInt(); @@ -166,7 +184,7 @@ private void onWorldMapLinkClicked(LinkTextComponent linkcomp) if (!exists) { - capi.SendChatMessage(string.Format("/waypoint addati {0} ={1} ={2} ={3} {4} {5} {6}", "circle", x, y, z, false, "steelblue", text)); + _capi.SendChatMessage(string.Format("/waypoint addati {0} ={1} ={2} ={3} {4} {5} {6}", "circle", x, y, z, false, "steelblue", text)); } elem?.CenterMapTo(new BlockPos(x, y, z)); @@ -180,54 +198,6 @@ private void OnClientTick(float dt) } } - private void OnLvlFinalize() - { - if (capi != null && mapAllowedClient()) - { - capi.Input.RegisterHotKey("worldmaphud", Lang.Get("Show/Hide Minimap"), GlKeys.F6, HotkeyType.HelpAndOverlays); - capi.Input.RegisterHotKey("minimapposition", Lang.Get("keycontrol-minimap-position"), GlKeys.F6, HotkeyType.HelpAndOverlays, false, true, false); - capi.Input.RegisterHotKey("worldmapdialog", Lang.Get("Show World Map"), GlKeys.M, HotkeyType.HelpAndOverlays); - capi.Input.SetHotKeyHandler("worldmaphud", OnHotKeyWorldMapHud); - capi.Input.SetHotKeyHandler("minimapposition", OnHotKeyMinimapPosition); - capi.Input.SetHotKeyHandler("worldmapdialog", OnHotKeyWorldMapDlg); - capi.RegisterLinkProtocol("worldmap", onWorldMapLinkClicked); - } - - foreach (var val in MapLayerRegistry) - { - if (val.Key == "entities" && !api.World.Config.GetAsBool("entityMapLayer")) continue; - MapLayers.Add((MapLayer)Activator.CreateInstance(val.Value, api, this)); - } - - - foreach (MapLayer layer in MapLayers) - { - layer.OnLoaded(); - } - - mapLayerGenThread = new Thread(new ThreadStart(() => - { - while (!IsShuttingDown) - { - foreach (MapLayer layer in MapLayers) - { - layer.OnOffThreadTick(20 / 1000f); - } - - Thread.Sleep(20); - } - })); - - mapLayerGenThread.IsBackground = true; - mapLayerGenThread.Start(); - - if (capi != null && (capi.Settings.Bool["showMinimapHud"] || !capi.Settings.Bool.Exists("showMinimapHud")) && (worldMapDlg == null || !worldMapDlg.IsOpened())) - { - ToggleMap(EnumDialogType.HUD); - } - - } - private void OnMapLayerDataReceivedClient(MapLayerUpdate msg) { for (int i = 0; i < msg.Maplayers.Length; i++) @@ -240,7 +210,7 @@ private void OnMapLayerDataReceivedClient(MapLayerUpdate msg) public bool mapAllowedClient() { - return capi.World.Config.GetBool("allowMap", true) || capi.World.Player.Privileges.IndexOf("allowMap") != -1; + return _capi.World.Config.GetBool("allowMap", true) || _capi.World.Player.Privileges.IndexOf("allowMap") != -1; } private bool OnHotKeyWorldMapHud(KeyCombination comb) @@ -251,8 +221,8 @@ private bool OnHotKeyWorldMapHud(KeyCombination comb) private bool OnHotKeyMinimapPosition(KeyCombination comb) { - int prev = capi.Settings.Int["minimapHudPosition"]; - capi.Settings.Int["minimapHudPosition"] = (prev + 1) % 4; + int prev = _capi.Settings.Int["minimapHudPosition"]; + _capi.Settings.Int["minimapHudPosition"] = (prev + 1) % 4; if (worldMapDlg == null || !worldMapDlg.IsOpened()) ToggleMap(EnumDialogType.HUD); else @@ -286,11 +256,11 @@ public void ToggleMap(EnumDialogType asType) { if (!isDlgOpened) { - if (asType == EnumDialogType.HUD) capi.Settings.Bool.Set("showMinimapHud", true, false); + if (asType == EnumDialogType.HUD) _capi.Settings.Bool.Set("showMinimapHud", true, false); worldMapDlg.Open(asType); foreach (MapLayer layer in MapLayers) layer.OnMapOpenedClient(); - clientChannel.SendPacket(new OnMapToggle() { OpenOrClose = true }); + _clientChannel.SendPacket(new OnMapToggle() { OpenOrClose = true }); return; } @@ -304,35 +274,36 @@ public void ToggleMap(EnumDialogType asType) if (asType == EnumDialogType.HUD) { - capi.Settings.Bool.Set("showMinimapHud", false, false); + _capi.Settings.Bool.Set("showMinimapHud", false, false); } - else if (capi.Settings.Bool["showMinimapHud"]) + else if (_capi.Settings.Bool["showMinimapHud"]) { worldMapDlg.Open(EnumDialogType.HUD); return; } - + } worldMapDlg.TryClose(); return; } - worldMapDlg = new GuiDialogWorldMap(onViewChangedClient, syncViewChange, capi, getTabsOrdered()); - worldMapDlg.OnClosed += () => { + worldMapDlg = new GuiDialogWorldMap(OnViewChangedClient, SyncViewChange, _capi, GetTabsOrdered()); + worldMapDlg.OnClosed += () => + { foreach (MapLayer layer in MapLayers) layer.OnMapClosedClient(); - clientChannel.SendPacket(new OnMapToggle() { OpenOrClose = false }); - + _clientChannel.SendPacket(new OnMapToggle() { OpenOrClose = false }); + }; worldMapDlg.Open(asType); foreach (MapLayer layer in MapLayers) layer.OnMapOpenedClient(); - clientChannel.SendPacket(new OnMapToggle() { OpenOrClose = true }); + _clientChannel.SendPacket(new OnMapToggle() { OpenOrClose = true }); - if (asType == EnumDialogType.HUD) capi.Settings.Bool.Set("showMinimapHud", true, false); // Don't trigger the watcher which will call Toggle again recursively! + if (asType == EnumDialogType.HUD) _capi.Settings.Bool.Set("showMinimapHud", true, false); // Don't trigger the watcher which will call Toggle again recursively! } - private List getTabsOrdered() + private List GetTabsOrdered() { Dictionary tabs = new Dictionary(); @@ -348,7 +319,7 @@ private List getTabsOrdered() return tabs.OrderBy(val => val.Value).Select(val => val.Key).ToList(); } - private void onViewChangedClient(List nowVisible, List nowHidden) + private void OnViewChangedClient(List nowVisible, List nowHidden) { foreach (MapLayer layer in MapLayers) { @@ -356,11 +327,11 @@ private void onViewChangedClient(List nowVisible, List now } } - private void syncViewChange(int x1, int z1, int x2, int z2) + private void SyncViewChange(int x1, int z1, int x2, int z2) { - clientChannel.SendPacket(new OnViewChangedPacket() { X1 = x1, Z1 = z1, X2 = x2, Z2 = z2 }); + _clientChannel.SendPacket(new OnViewChangedPacket() { X1 = x1, Z1 = z1, X2 = x2, Z2 = z2 }); } - + public void TranslateWorldPosToViewPos(Vec3d worldPos, ref Vec2f viewPos) { worldMapDlg.TranslateWorldPosToViewPos(worldPos, ref viewPos); @@ -368,7 +339,7 @@ public void TranslateWorldPosToViewPos(Vec3d worldPos, ref Vec2f viewPos) public void SendMapDataToServer(MapLayer forMapLayer, byte[] data) { - if (api.Side == EnumAppSide.Server) return; + if (_api.Side == EnumAppSide.Server) return; List maplayerdatas = new List(); @@ -378,17 +349,50 @@ public void SendMapDataToServer(MapLayer forMapLayer, byte[] data) ForMapLayer = MapLayerRegistry.FirstOrDefault(x => x.Value == forMapLayer.GetType()).Key }); - clientChannel.SendPacket(new MapLayerUpdate() { Maplayers = maplayerdatas.ToArray() }); + _clientChannel.SendPacket(new MapLayerUpdate() { Maplayers = maplayerdatas.ToArray() }); } + + private void OnLevelFinaliseClient() + { + if (_capi is null) return; + RegisterClientHotkeys(); + LoadMapLayersFromRegistry(); + CreateMapLayerGenerationThread(); + OpenMiniMap(); + } + + private void RegisterClientHotkeys() + { + if (_capi is null) return; + if (!mapAllowedClient()) return; + + _capi.Input.RegisterHotKey("worldmaphud", Lang.Get("Show/Hide Minimap"), GlKeys.F6, HotkeyType.HelpAndOverlays); + _capi.Input.RegisterHotKey("minimapposition", Lang.Get("keycontrol-minimap-position"), GlKeys.F6, HotkeyType.HelpAndOverlays, false, true, false); + _capi.Input.RegisterHotKey("worldmapdialog", Lang.Get("Show World Map"), GlKeys.M, HotkeyType.HelpAndOverlays); + _capi.Input.SetHotKeyHandler("worldmaphud", OnHotKeyWorldMapHud); + _capi.Input.SetHotKeyHandler("minimapposition", OnHotKeyMinimapPosition); + _capi.Input.SetHotKeyHandler("worldmapdialog", OnHotKeyWorldMapDlg); + + _capi.RegisterLinkProtocol("worldmap", OnWorldMapLinkClicked); + } + + private void OpenMiniMap() + { + if (!(worldMapDlg is null) && worldMapDlg.IsOpened()) return; + if (!_capi.Settings.Bool["showMinimapHud"] && _capi.Settings.Bool.Exists("showMinimapHud")) return; + ToggleMap(EnumDialogType.HUD); + } + #endregion #region Server Side + public override void StartServerSide(ICoreServerAPI sapi) { - sapi.Event.ServerRunPhase(EnumServerRunPhase.RunGame, OnLvlFinalize);; + sapi.Event.ServerRunPhase(EnumServerRunPhase.RunGame, OnLevelFinaliseServer); sapi.Event.ServerRunPhase(EnumServerRunPhase.Shutdown, () => IsShuttingDown = true); - serverChannel = + _serverChannel = sapi.Network.RegisterChannel("worldmap") .RegisterMessageType(typeof(MapLayerUpdate)) .RegisterMessageType(typeof(OnViewChangedPacket)) @@ -397,7 +401,7 @@ public override void StartServerSide(ICoreServerAPI sapi) .SetMessageHandler(OnViewChangedServer) .SetMessageHandler(OnMapLayerDataReceivedServer) ; - + } private void OnMapLayerDataReceivedServer(IServerPlayer fromPlayer, MapLayerUpdate msg) @@ -438,7 +442,7 @@ private void OnViewChangedServer(IServerPlayer fromPlayer, OnViewChangedPacket n public void SendMapDataToClient(MapLayer forMapLayer, IServerPlayer forPlayer, byte[] data) { - if (api.Side == EnumAppSide.Client) return; + if (_api.Side == EnumAppSide.Client) return; if (forPlayer.ConnectionState != EnumClientState.Playing) return; MapLayerData[] maplayerdatas = new MapLayerData[1] { @@ -449,9 +453,15 @@ public void SendMapDataToClient(MapLayer forMapLayer, IServerPlayer forPlayer, b } }; - serverChannel.SendPacket(new MapLayerUpdate() { Maplayers = maplayerdatas }, forPlayer); + _serverChannel.SendPacket(new MapLayerUpdate() { Maplayers = maplayerdatas }, forPlayer); + } + + private void OnLevelFinaliseServer() + { + LoadMapLayersFromRegistry(); + CreateMapLayerGenerationThread(); } #endregion } -} +} \ No newline at end of file