using BepInEx; using HarmonyLib; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UI; using TMPro; namespace StrategicMapPlus { [BepInPlugin("com.mod.strategicmapplus", "Strategic Map Plus", "28.0.0")] public class StrategicMapMod : BaseUnityPlugin { public static bool GlobalShowToggle = false; private float timer = 0f; private float interval = 0.5f; private bool texturesLoaded = false; public static Dictionary SpriteCache = new Dictionary(); public static Dictionary RawTextureCache = new Dictionary(); private HashSet reportedMissingKeys = new HashSet(); void Awake() { Logger.LogInfo("Strategic Map Plus v28: Posición 0,0,0."); } void Update() { // Toggle con Ctrl + V if ((Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && Input.GetKeyDown(KeyCode.V)) { GlobalShowToggle = !GlobalShowToggle; } if (!texturesLoaded && Time.time > 3f) LoadTextures(); timer += Time.deltaTime; if (timer < interval) return; timer = 0f; if (texturesLoaded) ScanStrategicMap(); } void LoadTextures() { Texture2D[] allTextures = Resources.FindObjectsOfTypeAll(); Dictionary textureLookup = new Dictionary(); foreach (var tex in allTextures) { if (tex != null && !string.IsNullOrEmpty(tex.name)) { if (!textureLookup.ContainsKey(tex.name)) textureLookup.Add(tex.name, tex); } } int loadedCount = 0; foreach (var kvp in Textures.TextureMap) { string logicKey = kvp.Key; string texName = kvp.Value; if (textureLookup.ContainsKey(texName)) { Texture2D tex = textureLookup[texName]; RawTextureCache[logicKey] = tex; // Guardamos la original para Debug Textures.IconOffset offset = null; if (Textures.IconsOffsets.ContainsKey(logicKey)) offset = Textures.IconsOffsets[logicKey]; Texture2D roundTex = MakeTextureTransparent(tex, offset); Sprite s = Sprite.Create(roundTex, new Rect(0, 0, roundTex.width, roundTex.height), new Vector2(0.5f, 0.5f)); SpriteCache[logicKey] = s; loadedCount++; } } if (loadedCount > 0) texturesLoaded = true; } void ScanStrategicMap() { GameObject regionsRoot = GameObject.Find("UI/2DRoot/Layout/Limit/FrontDialogs/StrategicView/Root/Regions"); if (regionsRoot == null || !regionsRoot.activeInHierarchy) return; foreach (Transform region in regionsRoot.transform) { Transform army = region.Find("Army"); if (army == null) continue; Component defenseRenderer = army.GetComponent("iron.match.renderers.strategicview.DefenseCountRenderer"); Transform visualRoot = army.Find("Root"); Transform targetParent = (visualRoot != null) ? visualRoot : army; if (defenseRenderer != null) ProcessRegion((MonoBehaviour)defenseRenderer, targetParent); } } void ProcessRegion(MonoBehaviour renderer, Transform parentTransform) { var visualizer = parentTransform.GetComponent(); if (visualizer == null) { visualizer = parentTransform.gameObject.AddComponent(); visualizer.Initialize(); } var traverse = Traverse.Create(renderer); var model = traverse.Property("model").GetValue(); if (model == null) { visualizer.Hide(); return; } var children = Traverse.Create(model).Field("Children").GetValue() as IEnumerable; if (children == null) children = Traverse.Create(model).Field("children").GetValue() as IEnumerable; string orderType = ""; bool isSpecial = false; int strength = 0; bool found = false; if (children != null) { foreach (var child in children) { var childTrav = Traverse.Create(child); var attributesObj = childTrav.Field("attributes").GetValue(); if (attributesObj != null) { var valuesDict = Traverse.Create(attributesObj).Field("data").GetValue() as IDictionary; if (valuesDict != null && valuesDict.Contains(400003)) { object typeCont = valuesDict[400003]; object typeVal = Traverse.Create(typeCont).Property("Value").GetValue(); if (typeVal?.ToString() == "Order") { if (valuesDict.Contains(400038)) // OrderType { object oCont = valuesDict[400038]; object oVal = Traverse.Create(oCont).Property("Value").GetValue(); var list = oVal as IList; if (list != null && list.Count > 0) orderType = list[0].ToString(); } if (valuesDict.Contains(400039)) // HasStar { object sCont = valuesDict[400039]; object sVal = Traverse.Create(sCont).Property("Value").GetValue(); if (sVal != null && sVal.ToString().ToLower() == "true") isSpecial = true; } if (valuesDict.Contains(400004)) // Strength { object strCont = valuesDict[400004]; object strVal = Traverse.Create(strCont).Property("Value").GetValue(); int.TryParse(strVal?.ToString(), out strength); } found = true; break; } } } } } if (!found) { visualizer.Hide(); return; } // Generar clave lógica string generatedKey = $"{orderType}_{strength}"; if (isSpecial) generatedKey += "_Star"; if (SpriteCache.ContainsKey(generatedKey)) { Texture2D raw = RawTextureCache.ContainsKey(generatedKey) ? RawTextureCache[generatedKey] : null; visualizer.ShowIcon(SpriteCache[generatedKey], raw); } else { if (!reportedMissingKeys.Contains(generatedKey)) { Logger.LogWarning($"[FALTA MAPEO] El juego pide: '{generatedKey}'"); reportedMissingKeys.Add(generatedKey); } visualizer.ShowText(generatedKey); } } // Función auxiliar que podrías llamar desde tu parche de Harmony public static Texture2D MakeTextureTransparent(Texture originalTexture, Textures.IconOffset offset = null) { if (originalTexture == null) return null; // 1. Crear una RenderTexture temporal para poder leer la textura original RenderTexture tmp = RenderTexture.GetTemporary( originalTexture.width, originalTexture.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); // 2. Copiar la textura original a la RenderTexture (esto "desbloquea" los píxeles) Graphics.Blit(originalTexture, tmp); // 3. Activar la RenderTexture para leerla RenderTexture previous = RenderTexture.active; RenderTexture.active = tmp; // 4. Crear una nueva Texture2D leíble Texture2D newTexture = new Texture2D(originalTexture.width, originalTexture.height); newTexture.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0); // 5. MODIFICAR LOS PÍXELES (Lógica de Re-muestreo / Resampling) // En lugar de mover la máscara, dejamos la máscara fija en el centro y movemos la textura (Source). // Esto permite hacer CLAMP (repetir bordes) para evitar cortes rectos. Color[] sourcePixels = newTexture.GetPixels(); // Píxeles originales (Source) Color[] finalPixels = new Color[sourcePixels.Length]; // Píxeles destino (Destination) float width = newTexture.width; float height = newTexture.height; Vector2 center = new Vector2(width / 2f, height / 2f); // Configuración por defecto float maskRadius = (Mathf.Min(width, height) / 2f); float shiftX = 0f; float shiftY = 0f; if (offset != null) { // Size: Escala de la máscara (1.0 = toca bordes) if (offset.size > 0) maskRadius *= offset.size; // Offset: Desplazamiento del CONTENIDO (UV Shift) // Si Offset X > 0 (positivo), queremos ver lo que hay a la derecha, // por tanto "desplazamos la textura hacia la izquierda" (sumamos al índice de lectura). shiftX = offset.x * width; shiftY = offset.y * height; } else { // Default offset = new Textures.IconOffset(0.07f, 0, 0.9f); if (offset.size > 0) maskRadius *= offset.size; shiftX = offset.x * width; shiftY = offset.y * height; } int w = (int)width; int h = (int)height; for (int i = 0; i < finalPixels.Length; i++) { int x = i % w; int y = i / w; // 1. MÁSCARA (Siempre centrada en el output) float dist = Vector2.Distance(new Vector2(x, y), center); if (dist > maskRadius) { finalPixels[i] = Color.clear; continue; } // 2. MUESTREO (Sampling) con Offset // Calculamos qué píxel de la textura original corresponde a esta posición (x,y) int srcX = (int)(x + shiftX); int srcY = (int)(y + shiftY); // 3. CLAMP (Repetir bordes) // Si nos salimos de la textura, repetimos el último píxel válido. // Esto evita el "corte recto" transparente (siempre que el borde tenga color). srcX = Mathf.Clamp(srcX, 0, w - 1); srcY = Mathf.Clamp(srcY, 0, h - 1); // Leemos del array original int srcIndex = srcY * w + srcX; finalPixels[i] = sourcePixels[srcIndex]; } newTexture.SetPixels(finalPixels); newTexture.Apply(); // 6. Limpieza RenderTexture.active = previous; RenderTexture.ReleaseTemporary(tmp); return newTexture; } } // --- VISUALIZADOR --- public class StrategicDisplay : MonoBehaviour { private GameObject displayObj; private Image iconImage; private TextMeshProUGUI debugText; private bool isInitialized = false; private bool hasContent = false; // --- ZONA DEBUG EN INSPECTOR --- public bool DebugMode = false; public float DebugOffsetX = 0f; public float DebugOffsetY = 0f; public float DebugSize = 1f; private Texture2D _rawTexture; // La textura original sin recortar private Texture2D _generatedDebugTex; // Para limpiar memoria private Sprite _defaultSprite; // Sprite original para restaurar private float _lastX, _lastY, _lastSize; private bool _lastDebugMode; void Update() { if (!isInitialized || displayObj == null) return; // Lógica de visualización (V o Ctrl+V) bool shouldShow = hasContent && (StrategicMapMod.GlobalShowToggle || Input.GetKey(KeyCode.V)); if (displayObj.activeSelf != shouldShow) { displayObj.SetActive(shouldShow); } // Lógica de DEBUG en tiempo real if (_rawTexture != null) { // Si activamos el modo debug, forzamos actualización if (DebugMode && !_lastDebugMode) { _lastDebugMode = true; UpdateDebugTexture(); } else if (!DebugMode && _lastDebugMode) { _lastDebugMode = false; // AL SALIR DEL DEBUG: Restaurar el sprite original if (iconImage != null && _defaultSprite != null) iconImage.sprite = _defaultSprite; } if (DebugMode) { if (_lastX != DebugOffsetX || _lastY != DebugOffsetY || _lastSize != DebugSize) { UpdateDebugTexture(); } } } } private void UpdateDebugTexture() { _lastX = DebugOffsetX; _lastY = DebugOffsetY; _lastSize = DebugSize; if (_generatedDebugTex != null) Destroy(_generatedDebugTex); var debugOffset = new Textures.IconOffset(DebugOffsetX, DebugOffsetY, DebugSize); _generatedDebugTex = StrategicMapMod.MakeTextureTransparent(_rawTexture, debugOffset); if (_generatedDebugTex != null) { Sprite debugSprite = Sprite.Create(_generatedDebugTex, new Rect(0, 0, _generatedDebugTex.width, _generatedDebugTex.height), new Vector2(0.5f, 0.5f)); if (iconImage != null) iconImage.sprite = debugSprite; } } public void Initialize() { if (isInitialized && displayObj != null) return; if (displayObj != null) Destroy(displayObj); try { displayObj = new GameObject("OrderIcon_Mod"); displayObj.transform.SetParent(this.transform, false); // POSICIÓN EXACTA 0,0,0 displayObj.transform.localPosition = Vector3.zero; // Aseguramos capa correcta displayObj.layer = this.gameObject.layer; RectTransform rt = displayObj.AddComponent(); rt.sizeDelta = new Vector2(45, 45); // Tamaño del icono iconImage = displayObj.AddComponent(); iconImage.raycastTarget = false; iconImage.color = Color.white; // Backup Text GameObject textObj = new GameObject("BackupText"); textObj.transform.SetParent(displayObj.transform, false); textObj.layer = this.gameObject.layer; RectTransform textRt = textObj.AddComponent(); textRt.anchoredPosition = Vector2.zero; debugText = textObj.AddComponent(); debugText.alignment = TextAlignmentOptions.Center; debugText.fontSize = 14; debugText.fontStyle = FontStyles.Bold; debugText.color = Color.magenta; // CRUCIAL: Ponemos el icono al final de la lista de hijos para que se pinte ENCIMA displayObj.transform.SetAsLastSibling(); // Inicialmente oculto displayObj.SetActive(false); isInitialized = true; } catch (System.Exception e) { Debug.LogWarning("Error UI: " + e.Message); if (displayObj != null) Destroy(displayObj); isInitialized = false; } } public void Hide() { hasContent = false; if (displayObj != null && displayObj.activeSelf) displayObj.SetActive(false); } public void ShowIcon(Sprite s, Texture2D raw = null) { if (!isInitialized) Initialize(); if (!isInitialized) return; // Guardamos la referencia raw para el debug _rawTexture = raw; _defaultSprite = s; // Guardamos el sprite base // Aseguramos orden de dibujado (Encima de todo) displayObj.transform.SetAsLastSibling(); if (s != null) { // Solo sobreescribimos visualmente si NO estamos en modo debug if (!DebugMode) { iconImage.sprite = s; } iconImage.enabled = true; if (debugText != null) debugText.text = ""; hasContent = true; } } public void ShowText(string t) { if (!isInitialized) Initialize(); if (!isInitialized) return; displayObj.transform.SetAsLastSibling(); if (iconImage != null) iconImage.enabled = false; if (debugText != null) debugText.text = t; hasContent = true; } } }