diff --git a/More World Locations_AIO/Src/Locations/BepinexConfigs.cs b/More World Locations_AIO/Src/Locations/BepinexConfigs.cs index c9aadf5..ad0f6d0 100644 --- a/More World Locations_AIO/Src/Locations/BepinexConfigs.cs +++ b/More World Locations_AIO/Src/Locations/BepinexConfigs.cs @@ -11,10 +11,13 @@ public class BepinexConfigs public static ConfigEntry EnableWaystones = null!; public static ConfigEntry EnableTraders = null!; public static ConfigEntry EnableTrainers = null!; + public static ConfigEntry PortIconsIndividual = null!; public static ConfigEntry UseCustomTraderConfigs = null!; public static ConfigEntry UseCustomLocationYAML = null!; public static ConfigEntry UseCustomLocalization = null!; + public static bool UseIndividualPortIcons => PortIconsIndividual?.Value == PortInit.Toggle.On; + public static void BindFeatureConfigs() { EnableShrines = PortInit.plugin.Config.BindConfig("0 - Features", "Enable Shrines", PortInit.Toggle.On, @@ -25,6 +28,8 @@ public static void BindFeatureConfigs() "If Off, trader locations (taverns, blacksmiths, material vendors) will not spawn", synced: true); EnableTrainers = PortInit.plugin.Config.BindConfig("0 - Features", "Enable Trainers", PortInit.Toggle.On, "If Off, trainer locations (skill book vendors) will not spawn", synced: true); + PortIconsIndividual = PortInit.plugin.Config.BindConfig("0 - Features", "Port Icons Individual", PortInit.Toggle.Off, + "If On, port map icons are discovered per player when the player gets close to a port. If Off, ports use the existing vanilla shared location icon behavior.", synced: true); UseCustomTraderConfigs = PortInit.plugin.Config.BindConfig("0 - Features", "Use Custom Trader Configs", PortInit.Toggle.Off, "If On, uses warpalicious.More_World_Locations_TraderItems.yml from config folder. Auto-extracts default if missing.", synced: true); UseCustomLocationYAML = PortInit.plugin.Config.BindConfig("0 - Features", "Use Custom Location YAML", PortInit.Toggle.Off, diff --git a/More World Locations_AIO/Src/Locations/LocationDefinitions.cs b/More World Locations_AIO/Src/Locations/LocationDefinitions.cs index f21e993..b268a2b 100644 --- a/More World Locations_AIO/Src/Locations/LocationDefinitions.cs +++ b/More World Locations_AIO/Src/Locations/LocationDefinitions.cs @@ -581,19 +581,19 @@ public static class LocationRings public static readonly MWLLocation[] Ports = { new() { Name = "MWL_Port1", AssetPath = "Assets/WarpProjects/More World Locations/Ports/MWL_Port1.prefab", - Config = new LocationConfig { Biome = Heightmap.Biome.Meadows, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 3f, MinAltitude = -2f, MaxAltitude = 1, SlopeRotation = true, Group = "MWL_Ports", IconPlaced = true} }, + Config = new LocationConfig { Biome = Heightmap.Biome.Meadows, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 3f, MinAltitude = -2f, MaxAltitude = 1, SlopeRotation = true, Group = "MWL_Ports", IconPlaced = !BepinexConfigs.UseIndividualPortIcons } }, new() { Name = "MWL_Port2", AssetPath = "Assets/WarpProjects/More World Locations/Ports/MWL_Port2.prefab", - Config = new LocationConfig { Biome = Heightmap.Biome.Plains, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 3f, MinAltitude = -2f, MaxAltitude = 1, SlopeRotation = true, Group = "MWL_Ports", BiomeArea = Heightmap.BiomeArea.Edge, IconPlaced = true } }, + Config = new LocationConfig { Biome = Heightmap.Biome.Plains, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 3f, MinAltitude = -2f, MaxAltitude = 1, SlopeRotation = true, Group = "MWL_Ports", BiomeArea = Heightmap.BiomeArea.Edge, IconPlaced = !BepinexConfigs.UseIndividualPortIcons } }, new() { Name = "MWL_Port3", AssetPath = "Assets/WarpProjects/More World Locations/Ports/MWL_Port3.prefab", - Config = new LocationConfig { Biome = Heightmap.Biome.Mistlands, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 10f, MinAltitude = -1f, MaxAltitude = 2, SlopeRotation = true, Group = "MWL_Ports", IconPlaced = true } }, + Config = new LocationConfig { Biome = Heightmap.Biome.Mistlands, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 10f, MinAltitude = -1f, MaxAltitude = 2, SlopeRotation = true, Group = "MWL_Ports", IconPlaced = !BepinexConfigs.UseIndividualPortIcons } }, new() { Name = "MWL_Port4", AssetPath = "Assets/WarpProjects/More World Locations/Ports/MWL_Port4.prefab", - Config = new LocationConfig { Biome = Heightmap.Biome.BlackForest, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 4f, MinAltitude = -1f, MaxAltitude = 1, SlopeRotation = true, Group = "MWL_Ports", IconPlaced = true } }, + Config = new LocationConfig { Biome = Heightmap.Biome.BlackForest, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 4f, MinAltitude = -1f, MaxAltitude = 1, SlopeRotation = true, Group = "MWL_Ports", IconPlaced = !BepinexConfigs.UseIndividualPortIcons } }, new() { Name = "MWL_Port5", AssetPath = "Assets/WarpProjects/More World Locations/Ports/MWL_Port5.prefab", - Config = new LocationConfig { Biome = Heightmap.Biome.AshLands, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 6f, MinAltitude = -0.5f, MaxAltitude = 2, SlopeRotation = true, Group = "MWL_Ports" , MaxDistance = 9100, IconPlaced = true} }, + Config = new LocationConfig { Biome = Heightmap.Biome.AshLands, Priotized = true, ExteriorRadius = 20, ClearArea = true, RandomRotation = false, MinDistanceFromSimilar = 1024, MaxTerrainDelta = 6f, MinAltitude = -0.5f, MaxAltitude = 2, SlopeRotation = true, Group = "MWL_Ports" , MaxDistance = 9100, IconPlaced = !BepinexConfigs.UseIndividualPortIcons } }, }; // ── Traders ────────────────────────────────────────────────────────── diff --git a/More World Locations_AIO/Src/Ports/src/KnownPorts.cs b/More World Locations_AIO/Src/Ports/src/KnownPorts.cs index ec2e6ad..f4f0cf4 100644 --- a/More World Locations_AIO/Src/Ports/src/KnownPorts.cs +++ b/More World Locations_AIO/Src/Ports/src/KnownPorts.cs @@ -1,14 +1,21 @@ using System.Collections.Generic; +using System.Linq; using HarmonyLib; using JetBrains.Annotations; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using More_World_Locations_AIO.Traders; +using UnityEngine; namespace More_World_Locations_AIO; public static class KnownPorts { private const string CustomDataKey = "MWL_KnownPorts"; + private const string IconCustomDataKey = "MWL_KnownPortIcons"; private static SerializedGuid? localKnownPorts; // cache player known ports + private static SerializedKnownPortIcons? localKnownPortIcons; // cache player discovered port icons + private static readonly List PortMapPins = new(); [HarmonyPatch(typeof(Player), nameof(Player.Load))] private static class Player_Load_Patch @@ -16,7 +23,16 @@ private static class Player_Load_Patch [UsedImplicitly] private static void Postfix(Player __instance) { - localKnownPorts = new SerializedGuid(__instance); + localKnownPorts = new SerializedGuid(__instance, CustomDataKey); + if (!BepinexConfigs.UseIndividualPortIcons) + { + localKnownPortIcons = null; + ClearMapPins(); + return; + } + + localKnownPortIcons = new SerializedKnownPortIcons(__instance, IconCustomDataKey); + RebuildMapPins(); } } @@ -26,28 +42,37 @@ private static class Player_Save_Patch [UsedImplicitly] private static void Prefix(Player __instance) { - localKnownPorts?.Save(__instance); + GetKnownPorts(__instance).Save(__instance); + if (!BepinexConfigs.UseIndividualPortIcons) return; + + GetKnownPortIcons(__instance).Save(__instance); + } + } + + [HarmonyPatch(typeof(Minimap), nameof(Minimap.Awake))] + private static class Minimap_Awake_Patch + { + [UsedImplicitly] + private static void Postfix() + { + RebuildMapPins(); } } private class SerializedGuid { private readonly List GUIDs = new(); - public SerializedGuid(Player player) + private readonly string? DataKey; + + public SerializedGuid(Player player, string? dataKey) { - if (!player.m_customData.TryGetValue(CustomDataKey, out string json)) return; + DataKey = dataKey; + if (DataKey == null) return; + if (!player.m_customData.TryGetValue(DataKey, out string json)) return; if (string.IsNullOrEmpty(json)) return; try { - List? data = JsonConvert.DeserializeObject>(json); - if (data is null) - { - player.ResetKnownPorts(); - } - else - { - GUIDs = data; - } + Load(json); } catch { @@ -55,10 +80,34 @@ public SerializedGuid(Player player) } } + private void Load(string json) + { + JToken token = JToken.Parse(json); + if (token.Type != JTokenType.Array) return; + + // Handles the short-lived object format used while port icons shared this field. + if (token.First?.Type == JTokenType.Object) + { + List? data = token.ToObject>(); + if (data != null) GUIDs.AddRange(data + .Where(port => !string.IsNullOrEmpty(port.GUID)) + .Select(port => port.GUID)); + return; + } + + List? guids = token.ToObject>(); + if (guids == null) return; + GUIDs.AddRange(guids.Where(guid => !string.IsNullOrEmpty(guid))); + } + public void Save(Player player) { - player.m_customData[CustomDataKey] = ToJson(); + if (DataKey == null) return; + player.m_customData[DataKey] = ToJson(); } + + public bool UsesKey(string? dataKey) => DataKey == dataKey; + private string ToJson() => JsonConvert.SerializeObject(GUIDs); public bool IsKnownPort(ShipmentManager.PortID portID) => GUIDs.Contains(portID.GUID); @@ -70,17 +119,162 @@ public void Add(ShipmentManager.PortID portID) } } + private class SerializedKnownPortIcons + { + private readonly List Ports = new(); + private readonly string? DataKey; + + public SerializedKnownPortIcons(Player player, string? dataKey) + { + DataKey = dataKey; + if (DataKey == null) return; + if (!player.m_customData.TryGetValue(DataKey, out string json)) return; + if (string.IsNullOrEmpty(json)) return; + try + { + List? data = JsonConvert.DeserializeObject>(json); + if (data != null) Ports.AddRange(data.Where(port => !string.IsNullOrEmpty(port.GUID))); + } + catch + { + player.m_customData.Remove(DataKey); + } + } + + public void Save(Player player) + { + if (DataKey == null) return; + player.m_customData[DataKey] = JsonConvert.SerializeObject(Ports); + } + + public bool UsesKey(string? dataKey) => DataKey == dataKey; + + public void AddIcon(ShipmentManager.PortID portID, Vector3 position) + { + KnownPortData? existing = Ports.FirstOrDefault(port => port.GUID == portID.GUID); + if (existing == null) + { + Ports.Add(new KnownPortData(portID, position)); + return; + } + + existing.Name = portID.Name; + existing.Position = new PortManager.SerializedVector(position); + existing.HasPosition = true; + } + + public List GetPortsWithPositions() => Ports + .Where(port => port.HasPosition) + .ToList(); + } + + private class KnownPortData + { + public string GUID = ""; + public string Name = ""; + public PortManager.SerializedVector Position; + public bool HasPosition; + + public KnownPortData() {} + + public KnownPortData(ShipmentManager.PortID portID, Vector3 position) + { + GUID = portID.GUID; + Name = portID.Name; + Position = new PortManager.SerializedVector(position); + HasPosition = true; + } + } + public static bool IsKnownPort(this Player player, ShipmentManager.PortID portID) { - localKnownPorts ??= new SerializedGuid(player); + localKnownPorts = GetKnownPorts(player); return localKnownPorts.IsKnownPort(portID); } public static void AddKnownPort(this Player player, ShipmentManager.PortID portID) { - localKnownPorts ??= new SerializedGuid(player); + localKnownPorts = GetKnownPorts(player); localKnownPorts.Add(portID); } - public static void ResetKnownPorts(this Player player) => player.m_customData.Remove(CustomDataKey); -} \ No newline at end of file + public static void AddDiscoveredPortIcon(this Player player, ShipmentManager.PortID portID, Vector3 position) + { + if (!BepinexConfigs.UseIndividualPortIcons) return; + + localKnownPortIcons = GetKnownPortIcons(player); + localKnownPortIcons.AddIcon(portID, position); + RebuildMapPins(); + } + + public static void ResetKnownPorts(this Player player) + { + player.m_customData.Remove(CustomDataKey); + player.m_customData.Remove(IconCustomDataKey); + localKnownPorts = new SerializedGuid(player, CustomDataKey); + localKnownPortIcons = new SerializedKnownPortIcons(player, IconCustomDataKey); + ClearMapPins(); + } + + private static SerializedGuid GetKnownPorts(Player player) + { + if (localKnownPorts == null || !localKnownPorts.UsesKey(CustomDataKey)) + { + localKnownPorts = new SerializedGuid(player, CustomDataKey); + } + return localKnownPorts; + } + + private static SerializedKnownPortIcons GetKnownPortIcons(Player player) + { + if (!BepinexConfigs.UseIndividualPortIcons) + { + localKnownPortIcons = null; + ClearMapPins(); + return new SerializedKnownPortIcons(player, null); + } + + if (localKnownPortIcons == null || !localKnownPortIcons.UsesKey(IconCustomDataKey)) + { + localKnownPortIcons = new SerializedKnownPortIcons(player, IconCustomDataKey); + } + return localKnownPortIcons; + } + + private static void RebuildMapPins() + { + if (Minimap.instance == null || Player.m_localPlayer == null) return; + if (MinimapTraderIcons.achorSprite == null) return; + if (!BepinexConfigs.UseIndividualPortIcons) + { + ClearMapPins(); + return; + } + + ClearMapPins(); + + foreach (KnownPortData port in GetKnownPortIcons(Player.m_localPlayer).GetPortsWithPositions()) + { + Minimap.PinData pin = Minimap.instance.AddPin( + port.Position.ToVector3(), + Minimap.PinType.None, + port.Name, + false, + false); + pin.m_icon = MinimapTraderIcons.achorSprite; + pin.m_doubleSize = true; + PortMapPins.Add(pin); + } + } + + private static void ClearMapPins() + { + if (Minimap.instance == null) return; + + foreach (Minimap.PinData pin in PortMapPins.Where(pin => pin != null).ToList()) + { + Minimap.instance.RemovePin(pin); + } + PortMapPins.Clear(); + } +} diff --git a/More World Locations_AIO/Src/Ports/src/Port.cs b/More World Locations_AIO/Src/Ports/src/Port.cs index 9d74622..cc8c1a4 100644 --- a/More World Locations_AIO/Src/Ports/src/Port.cs +++ b/More World Locations_AIO/Src/Ports/src/Port.cs @@ -38,9 +38,23 @@ public void Awake() public void Start() { if (!m_view.IsValid()) return; + if (BepinexConfigs.UseIndividualPortIcons) StartCoroutine(DiscoverIconCoroutine()); StartCoroutine(InitCoroutine()); } + private IEnumerator DiscoverIconCoroutine() + { + for (int i = 0; i < 20; i++) + { + if (Player.m_localPlayer != null) + { + Player.m_localPlayer.AddDiscoveredPortIcon(m_portID, GetMapPinPosition()); + yield break; + } + yield return i == 0 ? null : new WaitForSeconds(0.5f); + } + } + private IEnumerator InitCoroutine() { yield return null; @@ -159,6 +173,12 @@ public bool Interact(Humanoid user, bool hold, bool alt) return false; } + private Vector3 GetMapPinPosition() + { + LocationProxy locationProxy = WorldUtils.GetLocationInRange(transform.position, 10); + return locationProxy != null ? locationProxy.transform.position : transform.position; + } + public bool UseItem(Humanoid user, ItemDrop.ItemData item) => false; public string GetHoverText() @@ -482,4 +502,4 @@ private static class PortVars public static readonly int TraderName = "PortTraderName".GetStableHashCode(); } -} \ No newline at end of file +}