1016 lines
35 KiB
C#
1016 lines
35 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using BepInEx;
|
|
using BepInEx.Configuration;
|
|
using BepInEx.Logging;
|
|
using BepInEx.Unity.IL2CPP;
|
|
using Doozy.Engine.UI;
|
|
using Il2CppInterop.Runtime;
|
|
using Il2CppInterop.Runtime.Injection;
|
|
using Skopje.Modding;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace Skopje.Minimap
|
|
{
|
|
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
|
public class MinimapPlugin : BasePlugin
|
|
{
|
|
public const string PluginGuid = "com.skopje.minimap";
|
|
public const string PluginName = "Skopje Minimap";
|
|
public const string PluginVersion = "1.0.0";
|
|
|
|
internal static ManualLogSource LogSource;
|
|
|
|
internal static ConfigEntry<bool> Enabled;
|
|
internal static ConfigEntry<float> Size;
|
|
internal static ConfigEntry<float> ViewRadius;
|
|
internal static ConfigEntry<float> Gap;
|
|
internal static ConfigEntry<bool> ShowMarkers;
|
|
internal static ConfigEntry<float> MarkerSize;
|
|
|
|
internal static SkopjePlayer LocalPlayer;
|
|
|
|
// Il2Cpp delegates are collected if nothing managed keeps them alive.
|
|
static Il2CppSystem.Action _onClientStarted;
|
|
static Il2CppSystem.Action _onClientStopped;
|
|
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStarted;
|
|
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStopped;
|
|
|
|
public override void Load()
|
|
{
|
|
LogSource = Log;
|
|
Enabled = Config.Bind("Minimap", "Enabled", true, "Show the minimap under the clock.");
|
|
Size = Config.Bind("Minimap", "Size", 256f, new ConfigDescription(
|
|
"Width and height of the minimap, in the same units as the rest of the HUD.",
|
|
new AcceptableValueRange<float>(96f, 512f)));
|
|
ViewRadius = Config.Bind("Minimap", "ViewRadius", 100f, new ConfigDescription(
|
|
"Meters from the player to the edge of the minimap.",
|
|
new AcceptableValueRange<float>(25f, 600f)));
|
|
Gap = Config.Bind("Minimap", "Gap", 6f, new ConfigDescription(
|
|
"Space between the bottom of the clock and the top of the minimap.",
|
|
new AcceptableValueRange<float>(0f, 80f)));
|
|
ShowMarkers = Config.Bind("Minimap", "ShowMarkers", true, "Show the same map icons the full map uses, when they are inside the view.");
|
|
MarkerSize = Config.Bind("Minimap", "MarkerSize", 16f, new ConfigDescription(
|
|
"Icon size on the minimap.",
|
|
new AcceptableValueRange<float>(8f, 40f)));
|
|
|
|
ClassInjector.RegisterTypeInIl2Cpp<MinimapDisplay>();
|
|
|
|
_onClientStarted = ToIl2Cpp(new System.Action(OnClientStarted));
|
|
_onClientStopped = ToIl2Cpp(new System.Action(OnClientStopped));
|
|
_onPlayerStarted = ToIl2Cpp<SkopjePlayer>(new System.Action<SkopjePlayer>(OnPlayerStarted));
|
|
_onPlayerStopped = ToIl2Cpp<SkopjePlayer>(new System.Action<SkopjePlayer>(OnPlayerStopped));
|
|
|
|
ModAPI.add_OnClientStarted(_onClientStarted);
|
|
ModAPI.add_OnClientStopped(_onClientStopped);
|
|
ModAPI.add_OnPlayerStartedClient(_onPlayerStarted);
|
|
ModAPI.add_OnPlayerStoppedClient(_onPlayerStopped);
|
|
|
|
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Config: {Config.ConfigFilePath}");
|
|
}
|
|
|
|
public override bool Unload()
|
|
{
|
|
LocalPlayer = null;
|
|
if (MinimapDisplay.Instance != null)
|
|
UnityEngine.Object.Destroy(MinimapDisplay.Instance.gameObject);
|
|
return true;
|
|
}
|
|
|
|
static void OnClientStarted()
|
|
{
|
|
if (MinimapDisplay.Instance != null)
|
|
return;
|
|
|
|
var host = new GameObject("SkopjeMinimap");
|
|
UnityEngine.Object.DontDestroyOnLoad(host);
|
|
host.AddComponent<MinimapDisplay>();
|
|
LogSource.LogInfo("Minimap display created.");
|
|
}
|
|
|
|
static void OnClientStopped()
|
|
{
|
|
LocalPlayer = null;
|
|
if (MinimapDisplay.Instance != null)
|
|
UnityEngine.Object.Destroy(MinimapDisplay.Instance.gameObject);
|
|
}
|
|
|
|
static void OnPlayerStarted(SkopjePlayer player)
|
|
{
|
|
if (player != null && player.IsOwner)
|
|
LocalPlayer = player;
|
|
}
|
|
|
|
static void OnPlayerStopped(SkopjePlayer player)
|
|
{
|
|
if (LocalPlayer == player)
|
|
LocalPlayer = null;
|
|
}
|
|
|
|
internal static SkopjePlayer FindLocalPlayer()
|
|
{
|
|
return LocalPlayer;
|
|
}
|
|
|
|
static Il2CppSystem.Action ToIl2Cpp(System.Action action)
|
|
{
|
|
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(action);
|
|
}
|
|
|
|
static Il2CppSystem.Action<T> ToIl2Cpp<T>(System.Action<T> action)
|
|
{
|
|
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action<T>>(action);
|
|
}
|
|
}
|
|
|
|
public class MinimapDisplay : MonoBehaviour
|
|
{
|
|
const float Border = 2f;
|
|
const int MaxMarkers = 40;
|
|
const float SampleMeters = 20f;
|
|
|
|
public static MinimapDisplay Instance { get; private set; }
|
|
|
|
TimeDisplay _time;
|
|
UIView _hud;
|
|
MapControls _map;
|
|
LevelBounds _bounds;
|
|
Image _source;
|
|
Sprite _mapSprite;
|
|
|
|
RectTransform _root;
|
|
RectTransform _window;
|
|
RectTransform _content;
|
|
RectTransform _mapRect;
|
|
Image _mapImage;
|
|
RectTransform _markersRoot;
|
|
RectTransform _heading;
|
|
|
|
bool _useTopDown = true;
|
|
Vector2 _fallbackSize;
|
|
bool _laidOut;
|
|
bool _loggedReady;
|
|
bool _loggedTick;
|
|
bool _loggedWait;
|
|
float _nextSearch;
|
|
float _nextMarkerScan;
|
|
int _errorLogs;
|
|
|
|
readonly List<MarkerSlot> _slots = new List<MarkerSlot>();
|
|
readonly List<MapIndicator> _nearby = new List<MapIndicator>();
|
|
readonly List<float> _nearbyDist = new List<float>();
|
|
readonly Vector3[] _corners = new Vector3[4];
|
|
|
|
static Sprite _arrowSprite;
|
|
static Sprite _dotSprite;
|
|
|
|
public MinimapDisplay(IntPtr pointer) : base(pointer)
|
|
{
|
|
}
|
|
|
|
void Awake()
|
|
{
|
|
Instance = this;
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (_root != null)
|
|
Destroy(_root.gameObject);
|
|
if (Instance == this)
|
|
Instance = null;
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
try
|
|
{
|
|
Tick();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_errorLogs < 3)
|
|
{
|
|
_errorLogs++;
|
|
MinimapPlugin.LogSource.LogWarning("Minimap update failed: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Tick()
|
|
{
|
|
if (!_loggedTick)
|
|
{
|
|
_loggedTick = true;
|
|
MinimapPlugin.LogSource.LogInfo("Minimap ticking.");
|
|
}
|
|
|
|
if (!MinimapPlugin.Enabled.Value)
|
|
{
|
|
if (_root != null)
|
|
_root.gameObject.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
if (MinimapPlugin.LocalPlayer == null)
|
|
{
|
|
if (_root != null)
|
|
_root.gameObject.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
float now = Time.unscaledTime;
|
|
bool hudMissing = _hud == null || !_hud.gameObject.activeInHierarchy;
|
|
if ((_time == null || hudMissing || _mapSprite == null || _bounds == null) && now >= _nextSearch)
|
|
{
|
|
_nextSearch = now + 1.5f;
|
|
ResolveSources();
|
|
}
|
|
|
|
bool hudOn = _hud != null && _hud.gameObject.activeInHierarchy;
|
|
if (!hudOn)
|
|
{
|
|
if (_root != null)
|
|
_root.gameObject.SetActive(false);
|
|
if (!_loggedWait && now > 8f)
|
|
{
|
|
_loggedWait = true;
|
|
MinimapPlugin.LogSource.LogInfo("Minimap hidden: the in-game HUD is not active.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
EnsureWidget();
|
|
if (_root == null)
|
|
return;
|
|
|
|
if (!_root.gameObject.activeSelf)
|
|
_root.gameObject.SetActive(true);
|
|
|
|
PlaceUnderTimer();
|
|
|
|
SkopjePlayer player = MinimapPlugin.FindLocalPlayer();
|
|
if (_mapSprite == null || _bounds == null)
|
|
{
|
|
if (!_loggedWait && now > 8f)
|
|
{
|
|
_loggedWait = true;
|
|
MinimapPlugin.LogSource.LogInfo(
|
|
"Minimap frame is showing. Map image found: " + (_mapSprite != null) + ". Level bounds found: " + (_bounds != null) + ".");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!_laidOut || _mapImage.sprite != _mapSprite)
|
|
LayoutMap();
|
|
|
|
if (player != null && !Follow(player))
|
|
ShowWholeMap(player);
|
|
else if (player == null)
|
|
ShowWholeMap(null);
|
|
|
|
if (player != null && MinimapPlugin.ShowMarkers.Value)
|
|
{
|
|
if (now >= _nextMarkerScan)
|
|
{
|
|
_nextMarkerScan = now + 0.75f;
|
|
RescanMarkers(player);
|
|
}
|
|
MoveMarkers();
|
|
}
|
|
else if (_slots.Count > 0)
|
|
{
|
|
HideMarkers();
|
|
}
|
|
}
|
|
|
|
void ResolveSources()
|
|
{
|
|
if (_time == null)
|
|
_time = PickTimer();
|
|
if (_hud == null || !_hud.gameObject.activeInHierarchy)
|
|
{
|
|
UIView found = FindIngameView();
|
|
if (found != null)
|
|
_hud = found;
|
|
}
|
|
if (_mapSprite == null)
|
|
{
|
|
_map = PickMap();
|
|
_source = _map != null ? FindMapGraphic(_map) : null;
|
|
if (_source != null)
|
|
_mapSprite = _source.sprite;
|
|
if (_mapSprite == null)
|
|
_mapSprite = FindLooseMapSprite();
|
|
_laidOut = false;
|
|
}
|
|
if (_bounds == null)
|
|
_bounds = PickBounds();
|
|
}
|
|
|
|
void EnsureWidget()
|
|
{
|
|
if (_root != null)
|
|
return;
|
|
|
|
RectTransform parent = _hud.transform as RectTransform;
|
|
if (parent == null)
|
|
return;
|
|
|
|
int layer = parent.gameObject.layer;
|
|
_root = CreateRect("Minimap", parent, layer);
|
|
_root.anchorMin = _root.anchorMax = new Vector2(0f, 1f);
|
|
_root.pivot = new Vector2(0f, 1f);
|
|
|
|
Image border = CreateImage("Border", _root, DotSprite(), new Color(0.95f, 0.93f, 0.86f, 0.95f));
|
|
Stretch(border.rectTransform, 0f);
|
|
|
|
_window = CreateRect("Window", _root, layer);
|
|
Stretch(_window, Border);
|
|
_window.gameObject.AddComponent<RectMask2D>();
|
|
|
|
Image backdrop = CreateImage("Backdrop", _window, DotSprite(), new Color(0.07f, 0.09f, 0.08f, 0.94f));
|
|
Stretch(backdrop.rectTransform, 0f);
|
|
|
|
_content = CreateRect("Content", _window, layer);
|
|
_content.anchorMin = _content.anchorMax = new Vector2(0.5f, 0.5f);
|
|
_content.pivot = Vector2.zero;
|
|
_content.anchoredPosition = Vector2.zero;
|
|
|
|
_mapImage = CreateImage("Map", _content, _mapSprite, Color.white);
|
|
_mapRect = _mapImage.rectTransform;
|
|
_mapRect.anchorMin = _mapRect.anchorMax = Vector2.zero;
|
|
_mapRect.pivot = new Vector2(0.5f, 0.5f);
|
|
|
|
_markersRoot = CreateRect("Markers", _window, layer);
|
|
Stretch(_markersRoot, 0f);
|
|
|
|
_heading = CreateRect("Heading", _window, layer);
|
|
_heading.anchorMin = _heading.anchorMax = new Vector2(0.5f, 0.5f);
|
|
_heading.pivot = new Vector2(0.5f, 0.5f);
|
|
_heading.anchoredPosition = Vector2.zero;
|
|
_heading.sizeDelta = Vector2.zero;
|
|
|
|
Image shadow = CreateImage("ArrowShadow", _heading, ArrowSprite(), new Color(0f, 0f, 0f, 0.9f));
|
|
shadow.rectTransform.anchorMin = shadow.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
|
shadow.rectTransform.sizeDelta = new Vector2(22f, 22f);
|
|
Image arrow = CreateImage("Arrow", _heading, ArrowSprite(), Color.white);
|
|
arrow.rectTransform.anchorMin = arrow.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
|
arrow.rectTransform.sizeDelta = new Vector2(15f, 15f);
|
|
|
|
_root.SetAsLastSibling();
|
|
_laidOut = false;
|
|
MinimapPlugin.LogSource.LogInfo("Minimap parented to '" + parent.name + "'.");
|
|
}
|
|
|
|
void LayoutMap()
|
|
{
|
|
if (_mapSprite == null)
|
|
return;
|
|
|
|
_mapImage.sprite = _mapSprite;
|
|
_mapImage.color = Color.white;
|
|
_mapImage.type = Image.Type.Simple;
|
|
_mapImage.preserveAspect = false;
|
|
|
|
Vector2 bl = new Vector2(0f, 0f);
|
|
Vector2 tr = new Vector2(1f, 1f);
|
|
_useTopDown = _bounds != null;
|
|
if (_bounds != null)
|
|
{
|
|
try
|
|
{
|
|
bl = _bounds.GetTopDownPositionFrom01(new Vector2(0f, 0f));
|
|
tr = _bounds.GetTopDownPositionFrom01(new Vector2(1f, 1f));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
_useTopDown = false;
|
|
}
|
|
}
|
|
|
|
float span = Mathf.Max(Mathf.Abs(tr.x - bl.x), Mathf.Abs(tr.y - bl.y));
|
|
if (!_useTopDown || span < 0.0001f || float.IsNaN(span))
|
|
{
|
|
_useTopDown = false;
|
|
Rect spriteRect = _mapSprite.rect;
|
|
_fallbackSize = new Vector2(Mathf.Max(spriteRect.width, 1f), Mathf.Max(spriteRect.height, 1f));
|
|
_mapRect.anchoredPosition = _fallbackSize * 0.5f;
|
|
_mapRect.sizeDelta = _fallbackSize;
|
|
_mapRect.localScale = Vector3.one;
|
|
}
|
|
else
|
|
{
|
|
_mapRect.anchoredPosition = (bl + tr) * 0.5f;
|
|
_mapRect.sizeDelta = new Vector2(Mathf.Abs(tr.x - bl.x), Mathf.Abs(tr.y - bl.y));
|
|
_mapRect.localScale = new Vector3(tr.x < bl.x ? -1f : 1f, tr.y < bl.y ? -1f : 1f, 1f);
|
|
}
|
|
|
|
_laidOut = true;
|
|
if (!_loggedReady)
|
|
{
|
|
_loggedReady = true;
|
|
MinimapPlugin.LogSource.LogInfo("Minimap using sprite '" + _mapSprite.name + "'.");
|
|
}
|
|
}
|
|
|
|
void PlaceUnderTimer()
|
|
{
|
|
RectTransform parent = _hud.transform as RectTransform;
|
|
if (parent == null)
|
|
return;
|
|
|
|
if (_root.parent != parent)
|
|
{
|
|
_root.SetParent(parent, false);
|
|
AdoptLayer(_root, parent.gameObject.layer);
|
|
_root.SetAsLastSibling();
|
|
}
|
|
|
|
float size = Mathf.Clamp(MinimapPlugin.Size.Value, 96f, 512f);
|
|
_root.anchorMin = _root.anchorMax = new Vector2(0f, 1f);
|
|
_root.pivot = new Vector2(0f, 1f);
|
|
_root.localScale = Vector3.one;
|
|
_root.sizeDelta = new Vector2(size, size);
|
|
|
|
float x = 16f;
|
|
float y = -72f - MinimapPlugin.Gap.Value;
|
|
RectTransform clock = ClockRect();
|
|
if (clock != null)
|
|
{
|
|
clock.GetWorldCorners(_corners);
|
|
Vector3 bottomLeft = _corners[0];
|
|
Vector3 down = _corners[0] - _corners[1];
|
|
float paneHeight = down.magnitude;
|
|
float clockHeight = Mathf.Abs(clock.rect.height);
|
|
if (clockHeight < 1f)
|
|
clockHeight = Mathf.Abs(clock.sizeDelta.y);
|
|
if (paneHeight > 0.5f && clockHeight >= 1f)
|
|
{
|
|
bottomLeft += down / paneHeight * (paneHeight * (MinimapPlugin.Gap.Value / clockHeight));
|
|
Rect parentRect = parent.rect;
|
|
Vector3 local = parent.InverseTransformPoint(bottomLeft);
|
|
x = local.x - parentRect.xMin;
|
|
y = local.y - parentRect.yMax;
|
|
if (float.IsNaN(x) || float.IsNaN(y) || Mathf.Abs(x) > 4000f || Mathf.Abs(y) > 4000f)
|
|
{
|
|
x = 16f;
|
|
y = -72f - MinimapPlugin.Gap.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
_root.anchoredPosition = new Vector2(x, y);
|
|
}
|
|
|
|
RectTransform ClockRect()
|
|
{
|
|
if (_time == null || _time.TimePane == null)
|
|
return null;
|
|
return _time.TimePane;
|
|
}
|
|
|
|
bool Follow(SkopjePlayer player)
|
|
{
|
|
Vector3 pos = player.transform.position;
|
|
Vector2 here = ToMap(pos);
|
|
Vector2 east = ToMap(pos + Vector3.right * SampleMeters) - here;
|
|
Vector2 north = ToMap(pos + Vector3.forward * SampleMeters) - here;
|
|
float ppm = east.magnitude / SampleMeters;
|
|
if (ppm < 0.0000001f)
|
|
ppm = north.magnitude / SampleMeters;
|
|
if (ppm < 0.0000001f || float.IsNaN(ppm) || float.IsInfinity(ppm))
|
|
return false;
|
|
|
|
float inner = Mathf.Max(16f, _root.sizeDelta.x - Border * 2f);
|
|
float radius = Mathf.Clamp(MinimapPlugin.ViewRadius.Value, 25f, 600f);
|
|
float zoom = (inner * 0.5f) / (radius * ppm);
|
|
if (float.IsNaN(zoom) || float.IsInfinity(zoom))
|
|
return false;
|
|
|
|
_content.anchoredPosition = -here * zoom;
|
|
_content.localScale = new Vector3(zoom, zoom, 1f);
|
|
_heading.anchoredPosition = Vector2.zero;
|
|
|
|
Vector3 forward = player.transform.forward;
|
|
Vector2 mapForward = east * forward.x + north * forward.z;
|
|
if (mapForward.sqrMagnitude > 0.000001f)
|
|
{
|
|
float degrees = Mathf.Atan2(mapForward.x, mapForward.y) * Mathf.Rad2Deg;
|
|
_heading.localRotation = Quaternion.Euler(0f, 0f, -degrees);
|
|
}
|
|
|
|
_playerMap = here;
|
|
_zoom = zoom;
|
|
return true;
|
|
}
|
|
|
|
void ShowWholeMap(SkopjePlayer player)
|
|
{
|
|
if (_mapRect == null)
|
|
return;
|
|
|
|
Vector2 size = _mapRect.sizeDelta;
|
|
float inner = Mathf.Max(16f, _root.sizeDelta.x - Border * 2f);
|
|
float zoom = inner / Mathf.Max(Mathf.Max(size.x, size.y), 1f);
|
|
Vector2 center = _mapRect.anchoredPosition;
|
|
_content.anchoredPosition = -center * zoom;
|
|
_content.localScale = new Vector3(zoom, zoom, 1f);
|
|
_playerMap = center;
|
|
_zoom = zoom;
|
|
|
|
if (player == null || _bounds == null)
|
|
{
|
|
_heading.anchoredPosition = Vector2.zero;
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Vector2 here = ToMap(player.transform.position);
|
|
_heading.anchoredPosition = (here - center) * zoom;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
_heading.anchoredPosition = Vector2.zero;
|
|
}
|
|
}
|
|
|
|
Vector2 _playerMap;
|
|
float _zoom = 1f;
|
|
|
|
Vector2 ToMap(Vector3 world)
|
|
{
|
|
Vector2 uv = _bounds.GetLevelObjectPosition01(world);
|
|
if (!_useTopDown)
|
|
return new Vector2(uv.x * _fallbackSize.x, uv.y * _fallbackSize.y);
|
|
return _bounds.GetTopDownPositionFrom01(uv);
|
|
}
|
|
|
|
void RescanMarkers(SkopjePlayer player)
|
|
{
|
|
_nearby.Clear();
|
|
_nearbyDist.Clear();
|
|
|
|
float radius = Mathf.Clamp(MinimapPlugin.ViewRadius.Value, 25f, 600f) * 1.25f;
|
|
float limitSq = radius * radius;
|
|
Vector3 origin = player.transform.position;
|
|
|
|
var indicators = Resources.FindObjectsOfTypeAll<MapIndicator>();
|
|
for (int i = 0; i < indicators.Length; i++)
|
|
{
|
|
MapIndicator indicator = indicators[i];
|
|
if (!IsSceneObject(indicator) || !indicator.gameObject.activeInHierarchy)
|
|
continue;
|
|
if (IsLocalPlayerMarker(indicator, player))
|
|
continue;
|
|
|
|
Vector3 delta = indicator.transform.position - origin;
|
|
delta.y = 0f;
|
|
float distSq = delta.sqrMagnitude;
|
|
if (distSq > limitSq)
|
|
continue;
|
|
|
|
InsertNearest(indicator, distSq);
|
|
}
|
|
|
|
int generation = Time.frameCount;
|
|
float markerSize = Mathf.Clamp(MinimapPlugin.MarkerSize.Value, 8f, 40f);
|
|
for (int i = 0; i < _nearby.Count; i++)
|
|
{
|
|
MapIndicator indicator = _nearby[i];
|
|
MarkerSlot slot = SlotFor(indicator.GetInstanceID());
|
|
slot.Indicator = indicator;
|
|
slot.Generation = generation;
|
|
slot.Rect.gameObject.SetActive(true);
|
|
|
|
Sprite icon = null;
|
|
try
|
|
{
|
|
icon = indicator.GetIcon();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
icon = null;
|
|
}
|
|
|
|
slot.Image.sprite = icon != null ? icon : DotSprite();
|
|
slot.Image.color = icon != null ? indicator.Color : new Color(1f, 0.82f, 0.25f, 1f);
|
|
if (slot.Image.color.a < 0.05f)
|
|
slot.Image.color = icon != null ? Color.white : new Color(1f, 0.82f, 0.25f, 1f);
|
|
|
|
float scale = 1f;
|
|
try
|
|
{
|
|
if (indicator.Scale > 0.05f)
|
|
scale = Mathf.Clamp(indicator.Scale, 0.4f, 2.5f);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
scale = 1f;
|
|
}
|
|
|
|
slot.Rect.sizeDelta = Vector2.one * (markerSize * scale);
|
|
}
|
|
|
|
for (int i = 0; i < _slots.Count; i++)
|
|
{
|
|
if (_slots[i].Generation != generation)
|
|
_slots[i].Rect.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
void InsertNearest(MapIndicator indicator, float distSq)
|
|
{
|
|
int index = _nearby.Count;
|
|
for (int i = 0; i < _nearby.Count; i++)
|
|
{
|
|
if (distSq < _nearbyDist[i])
|
|
{
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index >= MaxMarkers)
|
|
return;
|
|
|
|
_nearby.Insert(index, indicator);
|
|
_nearbyDist.Insert(index, distSq);
|
|
if (_nearby.Count > MaxMarkers)
|
|
{
|
|
_nearby.RemoveAt(_nearby.Count - 1);
|
|
_nearbyDist.RemoveAt(_nearbyDist.Count - 1);
|
|
}
|
|
}
|
|
|
|
void MoveMarkers()
|
|
{
|
|
float limit = (_root.sizeDelta.x - Border * 2f) * 0.5f + 8f;
|
|
for (int i = 0; i < _slots.Count; i++)
|
|
{
|
|
MarkerSlot slot = _slots[i];
|
|
if (!slot.Rect.gameObject.activeSelf)
|
|
continue;
|
|
if (slot.Indicator == null)
|
|
{
|
|
slot.Rect.gameObject.SetActive(false);
|
|
continue;
|
|
}
|
|
|
|
Vector2 point = ToMap(slot.Indicator.transform.position);
|
|
Vector2 local = (point - _playerMap) * _zoom;
|
|
if (Mathf.Abs(local.x) > limit || Mathf.Abs(local.y) > limit)
|
|
{
|
|
slot.Image.enabled = false;
|
|
continue;
|
|
}
|
|
|
|
slot.Image.enabled = true;
|
|
slot.Rect.anchoredPosition = local;
|
|
}
|
|
}
|
|
|
|
void HideMarkers()
|
|
{
|
|
for (int i = 0; i < _slots.Count; i++)
|
|
_slots[i].Rect.gameObject.SetActive(false);
|
|
}
|
|
|
|
MarkerSlot SlotFor(int id)
|
|
{
|
|
for (int i = 0; i < _slots.Count; i++)
|
|
{
|
|
if (_slots[i].Id == id)
|
|
return _slots[i];
|
|
}
|
|
|
|
var rect = CreateRect("Marker", _markersRoot, _markersRoot.gameObject.layer);
|
|
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
|
rect.sizeDelta = Vector2.one * 16f;
|
|
|
|
Image image = rect.gameObject.AddComponent<Image>();
|
|
image.raycastTarget = false;
|
|
image.maskable = true;
|
|
image.preserveAspect = true;
|
|
|
|
var slot = new MarkerSlot
|
|
{
|
|
Id = id,
|
|
Rect = rect,
|
|
Image = image
|
|
};
|
|
_slots.Add(slot);
|
|
return slot;
|
|
}
|
|
|
|
static bool IsLocalPlayerMarker(MapIndicator indicator, SkopjePlayer player)
|
|
{
|
|
if (indicator.transform == player.transform || indicator.transform.IsChildOf(player.transform))
|
|
return true;
|
|
|
|
try
|
|
{
|
|
PlayerMapIndicator playerMarker = indicator.GetComponent<PlayerMapIndicator>();
|
|
return playerMarker != null && playerMarker.Player != null && playerMarker.Player.IsOwner;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static UIView FindIngameView()
|
|
{
|
|
var views = Resources.FindObjectsOfTypeAll<UIView>();
|
|
UIView inactive = null;
|
|
for (int i = 0; i < views.Length; i++)
|
|
{
|
|
UIView view = views[i];
|
|
if (!IsSceneObject(view) || !IsIngameView(view))
|
|
continue;
|
|
if (view.gameObject.activeInHierarchy)
|
|
return view;
|
|
inactive ??= view;
|
|
}
|
|
|
|
return inactive;
|
|
}
|
|
|
|
static bool IsIngameView(UIView view)
|
|
{
|
|
string objectName = view.name;
|
|
if (objectName == "View - Ingame" || objectName == "View - Ingam")
|
|
return true;
|
|
|
|
try
|
|
{
|
|
string viewName = view.ViewName;
|
|
return viewName == "Ingame" || viewName == "Ingam";
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static Sprite FindLooseMapSprite()
|
|
{
|
|
var sprites = Resources.FindObjectsOfTypeAll<Sprite>();
|
|
Sprite best = null;
|
|
float bestArea = 256f * 256f;
|
|
for (int i = 0; i < sprites.Length; i++)
|
|
{
|
|
Sprite sprite = sprites[i];
|
|
if (sprite == null)
|
|
continue;
|
|
string name = sprite.name;
|
|
if (name.IndexOf("map", StringComparison.OrdinalIgnoreCase) < 0)
|
|
continue;
|
|
if (name.IndexOf("icon", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("pin", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
continue;
|
|
|
|
Rect rect = sprite.rect;
|
|
float area = rect.width * rect.height;
|
|
if (area > bestArea)
|
|
{
|
|
best = sprite;
|
|
bestArea = area;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
static TimeDisplay PickTimer()
|
|
{
|
|
var displays = Resources.FindObjectsOfTypeAll<TimeDisplay>();
|
|
TimeDisplay fallback = null;
|
|
for (int i = 0; i < displays.Length; i++)
|
|
{
|
|
TimeDisplay display = displays[i];
|
|
if (!IsSceneObject(display) || display.TimePane == null)
|
|
continue;
|
|
if (display.gameObject.activeInHierarchy)
|
|
return display;
|
|
fallback ??= display;
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
static MapControls PickMap()
|
|
{
|
|
var maps = Resources.FindObjectsOfTypeAll<MapControls>();
|
|
MapControls fallback = null;
|
|
for (int i = 0; i < maps.Length; i++)
|
|
{
|
|
MapControls map = maps[i];
|
|
if (!IsSceneObject(map) || FindMapGraphic(map) == null)
|
|
continue;
|
|
if (map.gameObject.activeInHierarchy)
|
|
return map;
|
|
fallback ??= map;
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
static Image FindMapGraphic(MapControls map)
|
|
{
|
|
if (map == null)
|
|
return null;
|
|
|
|
if (map.MapBg != null && map.MapBg.sprite != null && map.MapBg.sprite.rect.width >= 128f)
|
|
return map.MapBg;
|
|
|
|
Image best = map.MapBg != null && map.MapBg.sprite != null ? map.MapBg : null;
|
|
float bestArea = SpriteArea(best);
|
|
if (map.MapTransform != null)
|
|
Consider(map.MapTransform, ref best, ref bestArea);
|
|
|
|
var extras = map.FallbackMapTransforms;
|
|
if (extras != null)
|
|
{
|
|
for (int i = 0; i < extras.Length; i++)
|
|
{
|
|
if (extras[i] != null)
|
|
Consider(extras[i], ref best, ref bestArea);
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
static void Consider(Transform root, ref Image best, ref float bestArea)
|
|
{
|
|
var images = root.GetComponentsInChildren<Image>(true);
|
|
for (int i = 0; i < images.Length; i++)
|
|
{
|
|
float area = SpriteArea(images[i]);
|
|
if (area > bestArea)
|
|
{
|
|
best = images[i];
|
|
bestArea = area;
|
|
}
|
|
}
|
|
}
|
|
|
|
static float SpriteArea(Image image)
|
|
{
|
|
if (image == null || image.sprite == null)
|
|
return 0f;
|
|
Rect rect = image.sprite.rect;
|
|
return rect.width * rect.height;
|
|
}
|
|
|
|
static LevelBounds PickBounds()
|
|
{
|
|
var all = Resources.FindObjectsOfTypeAll<LevelBounds>();
|
|
LevelBounds best = null;
|
|
float bestArea = -1f;
|
|
for (int i = 0; i < all.Length; i++)
|
|
{
|
|
LevelBounds bounds = all[i];
|
|
if (!IsSceneObject(bounds))
|
|
continue;
|
|
|
|
float area = 0f;
|
|
try
|
|
{
|
|
BoxCollider box = bounds.BoxCollider;
|
|
if (box != null)
|
|
{
|
|
Vector3 size = box.bounds.size;
|
|
area = Mathf.Abs(size.x * size.z);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
area = 0f;
|
|
}
|
|
|
|
if (area > bestArea)
|
|
{
|
|
bestArea = area;
|
|
best = bounds;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
static bool IsSceneObject(Component component)
|
|
{
|
|
return component != null && component.gameObject.scene.IsValid();
|
|
}
|
|
|
|
static RectTransform CreateRect(string name, Transform parent, int layer)
|
|
{
|
|
var go = new GameObject(name);
|
|
go.layer = layer;
|
|
RectTransform rect = go.AddComponent<RectTransform>();
|
|
rect.SetParent(parent, false);
|
|
rect.localScale = Vector3.one;
|
|
rect.localRotation = Quaternion.identity;
|
|
return rect;
|
|
}
|
|
|
|
static Image CreateImage(string name, Transform parent, Sprite sprite, Color color)
|
|
{
|
|
RectTransform rect = CreateRect(name, parent, parent.gameObject.layer);
|
|
Image image = rect.gameObject.AddComponent<Image>();
|
|
image.sprite = sprite;
|
|
image.color = color;
|
|
image.raycastTarget = false;
|
|
image.maskable = true;
|
|
return image;
|
|
}
|
|
|
|
static void Stretch(RectTransform rect, float inset)
|
|
{
|
|
rect.anchorMin = Vector2.zero;
|
|
rect.anchorMax = Vector2.one;
|
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
|
rect.offsetMin = new Vector2(inset, inset);
|
|
rect.offsetMax = new Vector2(-inset, -inset);
|
|
rect.localScale = Vector3.one;
|
|
}
|
|
|
|
static void AdoptLayer(Transform target, int layer)
|
|
{
|
|
target.gameObject.layer = layer;
|
|
for (int i = 0; i < target.childCount; i++)
|
|
AdoptLayer(target.GetChild(i), layer);
|
|
}
|
|
|
|
static Sprite DotSprite()
|
|
{
|
|
if (_dotSprite != null)
|
|
return _dotSprite;
|
|
|
|
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
|
texture.name = "MinimapDot";
|
|
texture.SetPixel(0, 0, Color.white);
|
|
texture.Apply();
|
|
texture.hideFlags = HideFlags.HideAndDontSave;
|
|
_dotSprite = Sprite.Create(texture, new Rect(0f, 0f, 1f, 1f), new Vector2(0.5f, 0.5f), 1f);
|
|
_dotSprite.hideFlags = HideFlags.HideAndDontSave;
|
|
return _dotSprite;
|
|
}
|
|
|
|
static Sprite ArrowSprite()
|
|
{
|
|
if (_arrowSprite != null)
|
|
return _arrowSprite;
|
|
|
|
const int n = 32;
|
|
var texture = new Texture2D(n, n, TextureFormat.RGBA32, false);
|
|
texture.name = "MinimapArrow";
|
|
texture.filterMode = FilterMode.Bilinear;
|
|
texture.wrapMode = TextureWrapMode.Clamp;
|
|
var clear = new Color(1f, 1f, 1f, 0f);
|
|
Vector2 a = new Vector2(15.5f, 29.5f);
|
|
Vector2 b = new Vector2(4f, 8.5f);
|
|
Vector2 c = new Vector2(27f, 8.5f);
|
|
for (int y = 0; y < n; y++)
|
|
{
|
|
for (int x = 0; x < n; x++)
|
|
texture.SetPixel(x, y, PointInTriangle(new Vector2(x + 0.5f, y + 0.5f), a, b, c) ? Color.white : clear);
|
|
}
|
|
|
|
texture.Apply();
|
|
texture.hideFlags = HideFlags.HideAndDontSave;
|
|
_arrowSprite = Sprite.Create(texture, new Rect(0f, 0f, n, n), new Vector2(0.5f, 0.5f), n);
|
|
_arrowSprite.hideFlags = HideFlags.HideAndDontSave;
|
|
return _arrowSprite;
|
|
}
|
|
|
|
static bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c)
|
|
{
|
|
float d1 = Sign(p, a, b);
|
|
float d2 = Sign(p, b, c);
|
|
float d3 = Sign(p, c, a);
|
|
bool hasNeg = d1 < 0f || d2 < 0f || d3 < 0f;
|
|
bool hasPos = d1 > 0f || d2 > 0f || d3 > 0f;
|
|
return !(hasNeg && hasPos);
|
|
}
|
|
|
|
static float Sign(Vector2 p, Vector2 a, Vector2 b)
|
|
{
|
|
return (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y);
|
|
}
|
|
|
|
sealed class MarkerSlot
|
|
{
|
|
public int Id;
|
|
public int Generation;
|
|
public MapIndicator Indicator;
|
|
public RectTransform Rect;
|
|
public Image Image;
|
|
}
|
|
}
|
|
}
|