// Copyright (c) 2024 Augie R. Maddox, Guavaman Enterprises. All rights reserved. #if UNITY_2020 || UNITY_2021 || UNITY_2022 || UNITY_2023 || UNITY_6000 || UNITY_6000_0_OR_NEWER #define UNITY_2020_PLUS #endif #if UNITY_2019 || UNITY_2020_PLUS #define UNITY_2019_PLUS #endif #if UNITY_2018 || UNITY_2019_PLUS #define UNITY_2018_PLUS #endif #if UNITY_2018_PLUS #define REWIRED_SUPPORTS_TMPRO #endif #if REWIRED_SUPPORTS_TMPRO #if UNITY_2020_PLUS // A version of Text Mesh Pro or Unity UI that uses asset version 1.1.0 // may or may not be installed in this version of Unity. // Since there are no preprocessor conditionals defined for the // Unity UI / TMPro version, the only choice is to use reflection // to support classes that may or may not exist in some projects. #define MAY_SUPPORT_TMPRO_ASSET_V_1_1_0 #endif #if UNITY_6000_0_OR_NEWER // This Unity version is guaranteed to support at least Unity UI 2.0, // which uses asset version 1.1.0. Avoid most of the reflection nonsense. // However, some reflection is still required because // TMPro.TMP_SpriteAsset now has internal setters for the tables and // version in Unity UI 2.0. This effectively prevents scripts from // modfying the Sprite Asset tables at runtime. #define SUPPORTS_AT_LEAST_TMPRO_ASSET_V_1_1_0 #endif #pragma warning disable 0649 namespace Rewired.Glyphs.UnityUI { using System.Reflection; using Rewired; using System; using System.Text; using System.Collections.Generic; /// /// A helper class to display Rewired glyphs and display names inline in TMPro Text (Unity UI). /// Parses tags within text and replaces them with Sprites or text. /// Enter text in this component's text field, not in the TMPro Text field. Text entered here /// will be parsed, then the Text Mesh Pro Text component will be updated with the text, including Sprite tags. /// [UnityEngine.AddComponentMenu("Rewired/Glyphs/Unity UI/Unity UI Text Mesh Pro Glyph Helper")] [UnityEngine.RequireComponent(typeof(TMPro.TextMeshProUGUI))] public class UnityUITextMeshProGlyphHelper : UnityEngine.MonoBehaviour { [UnityEngine.Tooltip("Enter text into this field and not in the TMPro Text field directly. " + "Text will be parsed for special tags, and the final result will be passed on to the Text Mesh Pro Text component. " + "See the documentation for special tag format.")] [UnityEngine.SerializeField] [UnityEngine.TextArea(3, 10)] private string _text; [UnityEngine.Tooltip("Optional reference to an object that defines options. If blank, the global default options will be used.")] [UnityEngine.SerializeField] private ControllerElementGlyphSelectorOptionsSOBase _options; [UnityEngine.Tooltip("Options that control how Text Mesh Pro displays Sprites.")] [UnityEngine.SerializeField] private TMProSpriteOptions _spriteOptions = TMProSpriteOptions.Default; [UnityEngine.Tooltip("Optional material for Sprites. If blank, the default material will be used.\n" + "Material is instantiated for each Sprite Asset, so making changes to values in the base material later will not affect Sprites. " + "Changing the base material at runtime will copy only certain properties from the new material to Sprite materials." )] [UnityEngine.SerializeField] private UnityEngine.Material _baseSpriteMaterial; [UnityEngine.Tooltip("If enabled, local values such as Sprite color will be used instead of the value on the base material.")] [UnityEngine.SerializeField] private bool _overrideSpriteMaterialProperties = true; [UnityEngine.Tooltip("These properties will override the properties on the Sprite material if Override Sprite Material Properties is enabled.")] [UnityEngine.SerializeField] private SpriteMaterialProperties _spriteMaterialProperties = SpriteMaterialProperties.Default; [NonSerialized] private TMPro.TextMeshProUGUI _tmProText; [NonSerialized] private string _textPrev; [NonSerialized] private readonly StringBuilder _processTagSb = new StringBuilder(); [NonSerialized] private readonly StringBuilder _tempSb = new StringBuilder(); [NonSerialized] private readonly StringBuilder _tempSb2 = new StringBuilder(); [NonSerialized] private Asset _primaryAsset; [NonSerialized] private readonly List _assignedAssets = new List(); [NonSerialized] private readonly List _assetsPool = new List(); [NonSerialized] private readonly List _tempAems = new List(); [NonSerialized] private readonly List _tempGlyphs = new List(); [NonSerialized] private readonly List _dirtyAssets = new List(); [NonSerialized] private readonly List _tempKeys = new List(); [NonSerialized] private readonly List _glyphsOrTextTemp = new List(); [NonSerialized] private readonly List _currentlyUsedAssets = new List(); [NonSerialized] private readonly List _currentTags = new List(); [NonSerialized] private Dictionary _tempStringDictionary = new Dictionary(); [NonSerialized] private bool _initialized; [NonSerialized] private bool _rebuildRequired; [NonSerialized] private UnityEngine.Texture2D _stubTexture; private Tag.Pool __controllerElementTagPool; private Tag.Pool controllerElementTagPool { get { return __controllerElementTagPool != null ? __controllerElementTagPool : (__controllerElementTagPool = new Tag.Pool()); } } private Tag.Pool __actionTagPool; private Tag.Pool actionTagPool { get { return __actionTagPool != null ? __actionTagPool : (__actionTagPool = new Tag.Pool()); } } private Tag.Pool __playerTagPool; private Tag.Pool playerTagPool { get { return __playerTagPool != null ? __playerTagPool : (__playerTagPool = new Tag.Pool()); } } [NonSerialized] private Dictionary __tagHandlers; private Dictionary tagHandlers { get { return __tagHandlers != null ? __tagHandlers : (__tagHandlers = new Dictionary() { { "rewiredelement", ProcessTag_ControllerElement }, { "rewiredaction", ProcessTag_Action }, { "rewiredplayer", ProcessTag_Player } }); } } /// /// Text will be parsed for special tags, and the final result will be passed on to the Text Mesh Pro Text component. /// /// /// /// Attributes: /// type="[glyphOrText|glyph|text]" (optional, default: glyphOrText) /// playerId=[player_id] (id or name required) /// playerName="[player_name]" (id or name required) /// actionId=[action_id] (id or name required) /// actionName="[action_name]" (id or name required) /// actionRange="[full|positive|negative]" (optional, default: full) /// Example: /// /// Display the name of an Action: /// /// Attributes: /// id=[action_id] (id or name required) /// name=[action_name] (id or name required) /// range="[full|positive|negative]" (optional, default: full) /// Example: /// /// Display the name of a Player: /// /// Attributes: /// id=[action_id] (id or name required) /// name=[action_name] (id or name required) /// Example: /// ]]> /// public virtual string text { get { return _text; } set { _text = value; RequireRebuild(); } } /// /// Optional reference to an object that defines options. /// If blank, the global default options will be used. /// public virtual ControllerElementGlyphSelectorOptionsSOBase options { get { return _options; } set { _options = value; RequireRebuild(); } } /// /// Options that control how Text Mesh Pro displays Sprites. /// public virtual TMProSpriteOptions spriteOptions { get { return _spriteOptions; } set { _spriteOptions = value; // Set values in all sprites in all assets int count = _assignedAssets.Count; int spriteCount; ITMProSprite sprite; UnityEngine.Rect rect; for (int i = 0; i < count; i++) { spriteCount = _assignedAssets[i].spriteAsset.spriteCount; for (int j = 0; j < spriteCount; j++) { sprite = _assignedAssets[i].spriteAsset.GetSprite(j); if (sprite == null || sprite.sprite == null) continue; rect = sprite.sprite.rect; sprite.xOffset = rect.width * _spriteOptions.offsetSizeMultiplier.x + _spriteOptions.extraOffset.x; sprite.yOffset = rect.height * _spriteOptions.offsetSizeMultiplier.y + _spriteOptions.extraOffset.y; sprite.xAdvance = rect.width * _spriteOptions.xAdvanceWidthMultiplier + _spriteOptions.extraXAdvance; sprite.scale = _spriteOptions.scale; } // Make TMPro update the Sprite scale TMPro.TMPro_EventManager.ON_SPRITE_ASSET_PROPERTY_CHANGED(true, _assignedAssets[i].spriteAsset.GetSpriteAsset()); } } } /// /// Optional material for Sprites. If blank, the default material will be used. /// Material is instantiated for each Sprite Asset, so making changes to values in the base material later will not affect Sprites. /// Changing the base material at runtime will copy only certain properties from the new material to Sprite materials. /// public virtual UnityEngine.Material baseSpriteMaterial { get { return _baseSpriteMaterial; } set { _baseSpriteMaterial = value; UnityEngine.Material sourceMaterial = _baseSpriteMaterial != null ? _baseSpriteMaterial : _primaryAsset.material; ForEachAsset(asset => { CopyMaterialProperties(sourceMaterial, asset.material); if (_overrideSpriteMaterialProperties) { CopySpriteMaterialPropertiesToMaterial(_spriteMaterialProperties, asset.material); } TMPro.TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, asset.material); }); } } /// /// If enabled, local values such as Sprite color will be used instead of the value on the base material. /// public virtual bool overrideSpriteMaterialProperties { get { return _overrideSpriteMaterialProperties; } set { _overrideSpriteMaterialProperties = value; if (value) { ForEachAsset(asset => { CopySpriteMaterialPropertiesToMaterial(_spriteMaterialProperties, asset.material); TMPro.TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, asset.material); }); } else { UnityEngine.Material sourceMaterial = _baseSpriteMaterial != null ? _baseSpriteMaterial : _primaryAsset.material; ForEachAsset(asset => { CopyMaterialProperties(sourceMaterial, asset.material); TMPro.TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, asset.material); }); } } } /// /// These properties will override the properties on the Sprite material if is true. /// public virtual SpriteMaterialProperties spriteMaterialProperties { get { return _spriteMaterialProperties; } set { _spriteMaterialProperties = value; if (!_overrideSpriteMaterialProperties) return; ForEachAsset(asset => { CopySpriteMaterialPropertiesToMaterial(_spriteMaterialProperties, asset.material); TMPro.TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, asset.material); }); } } protected virtual void OnEnable() { Initialize(); } protected virtual void Start() { MainUpdate(); } protected virtual void Update() { if (!ReInput.isReady) return; #if UNITY_EDITOR // Handle recompile in Play mode if (!Initialize()) return; if (UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.M)) { baseSpriteMaterial = _baseSpriteMaterial; } // Handle inspector value changes if (_editorInspectorChanged) { spriteOptions = _spriteOptions; baseSpriteMaterial = _baseSpriteMaterial; overrideSpriteMaterialProperties = _overrideSpriteMaterialProperties; spriteMaterialProperties = _spriteMaterialProperties; RequireRebuild(); _editorInspectorChanged = false; } #endif MainUpdate(); } protected virtual void OnDestroy() { // Clean up all assets if (_primaryAsset != null) { if (_tmProText != null) { if (_tmProText.spriteAsset == _primaryAsset.spriteAsset.GetSpriteAsset()) { _tmProText.spriteAsset = null; } } _primaryAsset.Destroy(); _primaryAsset = null; } for (int i = 0; i < _assignedAssets.Count; i++) { if (_assignedAssets[i] == null) continue; _assignedAssets[i].Destroy(); } _assignedAssets.Clear(); for (int i = 0; i < _assetsPool.Count; i++) { if (_assetsPool[i] == null) continue; _assetsPool[i].Destroy(); } _assetsPool.Clear(); if (_stubTexture != null) { Destroy(_stubTexture); _stubTexture = null; } for (int i = 0; i < _currentTags.Count; i++) { _currentTags[i].ReturnToPool(); } } #if UNITY_EDITOR [NonSerialized] private bool _editorInspectorChanged; protected virtual void OnValidate() { _editorInspectorChanged = true; } #endif /// /// Updates glyphs / text and TMPro text value immedately. /// Normally, this happens automatically in the Update loop. /// public virtual void ForceUpdate() { if (!ReInput.isReady) return; _rebuildRequired = true; Update(); } /// /// Gets the Controller Element Glyph Options if set, otherwise the default options. /// /// The Controller Element Glyph Options if set, otherwise the default options. protected virtual ControllerElementGlyphSelectorOptions GetOptionsOrDefault() { if (_options != null && _options.options == null) { UnityEngine.Debug.LogError("Rewired: Options missing on " + typeof(ControllerElementGlyphSelectorOptions).Name + ". Global default options will be used instead."); return ControllerElementGlyphSelectorOptions.defaultOptions; } return _options != null ? _options.options : ControllerElementGlyphSelectorOptions.defaultOptions; } private bool Initialize() { if (_initialized) return true; #if UNITY_EDITOR if (UnityEditor.EditorApplication.isCompiling) return false; #endif _tmProText = GetComponent(); _stubTexture = new UnityEngine.Texture2D(1, 1); CreatePrimaryAsset(); _initialized = true; return true; } private void MainUpdate() { bool changed = false; // Monitor changes to glyph / text results. // This is necessary because controller element glyph changes may occur when // last active controller changes, when glyph sets change, when glyphs are // disabled / enabled, when mappings change, or in the case of text, // when localization changes. { Tag tag; int count = _currentTags.Count; for (int i = 0; i < count; i++) { tag = _currentTags[i]; switch (tag.tagType) { case Tag.TagType.ControllerElement: { // Monitor controller element glyph changes ControllerElementTag tTag = (ControllerElementTag)tag; _glyphsOrTextTemp.Clear(); TryGetControllerElementGlyphsOrText((ControllerElementTag)tag, _glyphsOrTextTemp); if (!IsEqual(_glyphsOrTextTemp, tTag.glyphsOrText)) { changed = true; break; } } break; case Tag.TagType.Action: { // Monitor Action changes ActionTag tTag = (ActionTag)tag; string displayName; TryGetActionDisplayName(tTag, out displayName); if (!string.Equals(tTag.displayName, displayName, StringComparison.Ordinal)) { changed = true; break; } } break; case Tag.TagType.Player: { // Monitor Player changes PlayerTag tTag = (PlayerTag)tag; string displayName; TryGetPlayerDisplayName(tTag, out displayName); if (!string.Equals(tTag.displayName, displayName, StringComparison.Ordinal)) { changed = true; break; } break; } default: throw new NotImplementedException(); } } } // Monitor text changes if (!string.Equals(_text, _textPrev, StringComparison.Ordinal)) { _textPrev = _text; changed = true; } // Update text on change if (changed || _rebuildRequired) { // Parse text and replace Rewired tags with TMPro sprite tags string newText; if (ParseText(_textPrev, out newText)) { _tmProText.text = newText; } } // Process Sprite Assets that changed { int count = _dirtyAssets.Count; if (count > 0) { for (int i = 0; i < count; i++) { _dirtyAssets[i].spriteAsset.UpdateLookupTables(); } _dirtyAssets.Clear(); } } } private bool ParseText(string text, out string newText) { newText = null; // Clear current tags and build new onesfrom the text Tag.Clear(_currentTags); // Clear list so used assets can be detected after parsing _currentlyUsedAssets.Clear(); bool changed = false; // Controller Element while (ProcessNextTag(ref text, _processTagSb)) { changed = true; newText = text; } RemoveUnusedAssets(); if (_rebuildRequired) { _rebuildRequired = false; } return changed; } private bool ProcessNextTag(ref string text, StringBuilder sb) { const char tagStart = '<'; const char tagEnd = '>'; const int state_searchForStartTag = 0; const int state_searchForTagName = 1; const int state_parseAttributes = 2; int state = state_searchForStartTag; ParseTagAttributesHandler parseTagAttributesHandler = null; string replacementString; char c; int tagStartIndex = -1; try { for (int i = 0; i < text.Length; i++) { c = text[i]; switch (state) { case state_searchForStartTag: if (c == tagStart) { tagStartIndex = i; sb.Length = 0; state = state_searchForTagName; } break; case state_searchForTagName: if (IsValidTagNameChar(c)) { sb.Append(char.ToLowerInvariant(c)); } else if (char.IsWhiteSpace(c)) { if (sb.Length > 0) { if (tagHandlers.TryGetValue(sb.ToString(), out parseTagAttributesHandler)) { sb.Length = 0; state = state_parseAttributes; } else { // invalid tag name state = state_searchForStartTag; // start over i -= 1; } } } else { // error state = state_searchForStartTag; // start over i -= 1; } break; case state_parseAttributes: { int tagEndIndex = text.IndexOf(tagEnd, i); // TODO: This is possibly unsafe. If '>' is contained within any attribute string, it will fail. if (tagEndIndex < 0) throw new Exception("Malformed tag."); // error if (parseTagAttributesHandler(text, i, tagEndIndex - i, out replacementString)) { // replace string sb.Length = 0; if (tagStartIndex > 0) { sb.Append(text, 0, tagStartIndex); } sb.Append(replacementString); int tailStartIndex = tagEndIndex + 1; if (tailStartIndex < text.Length) { sb.Append(text, tailStartIndex, text.Length - tailStartIndex); } text = sb.ToString(); return true; } else { // error throw new Exception("Error parsing attributes."); } } } } } catch (Exception ex) { UnityEngine.Debug.LogError(ex); } return false; } private bool ProcessTag_ControllerElement(string text, int startIndex, int count, out string replacement) { ControllerElementTag tag; if (!ControllerElementTag.TryParseString(text, startIndex, count, _tempSb, _tempSb2, _tempStringDictionary, controllerElementTagPool, out tag)) { replacement = null; return false; // only fail if parsing fails } _currentTags.Add(tag); // Replace text with glyph / text tag.glyphsOrText.Clear(); if (!TryGetControllerElementGlyphsOrText(tag, tag.glyphsOrText)) { replacement = null; return true; } TryCreateTMProString(tag.glyphsOrText, out replacement); return true; } private bool ProcessTag_Action(string text, int startIndex, int count, out string replacement) { ActionTag tag; if (!ActionTag.TryParseString(text, startIndex, count, _tempSb, _tempSb2, _tempStringDictionary, actionTagPool, out tag)) { replacement = null; return false; // only fail if parsing fails } _currentTags.Add(tag); TryGetActionDisplayName(tag, out replacement); return true; } private bool ProcessTag_Player(string text, int startIndex, int count, out string replacement) { PlayerTag tag; if (!PlayerTag.TryParseString(text, startIndex, count, _tempSb, _tempSb2, _tempStringDictionary, playerTagPool, out tag)) { replacement = null; return false; // only fail if parsing fails } _currentTags.Add(tag); TryGetPlayerDisplayName(tag, out replacement); return true; } private bool TryCreateTMProString(List glyphs, out string result) { // Replace text with glyph / text StringBuilder sb = _tempSb; sb.Length = 0; string spriteKey; int resultCount = glyphs.Count; for (int i = 0; i < resultCount; i++) { spriteKey = glyphs[i].glyphKey; // Prioritize glyphs if (glyphs[i].sprite != null && !string.IsNullOrEmpty(spriteKey) && TryAssignSprite(glyphs[i].sprite, spriteKey)) { WriteSpriteKey(sb, spriteKey); } else { // Fall back to text sb.Append(glyphs[i].name); } if (i < resultCount - 1) sb.Append(" "); } result = sb.ToString(); return !string.IsNullOrEmpty(result); } private bool TryGetControllerElementGlyphsOrText(ControllerElementTag tag, List results) { if (tag == null) return false; ActionElementMap aemResult1; ActionElementMap aemResult2; _tempAems.Clear(); if (!GlyphTools.TryGetActionElementMaps(tag.playerId, tag.actionId, tag.actionRange, GetOptionsOrDefault(), _tempAems, out aemResult1, out aemResult2)) { return false; } object glyph; string glyphKey; string name; // Two split axis bindings if (aemResult1 != null && aemResult2 != null) { GlyphOrText r = new GlyphOrText(); // Handle special combined glyphs (D-Pad Left + D-Pad Right = D-Pad Horizontal) _tempAems.Clear(); _tempAems.Add(aemResult1); _tempAems.Add(aemResult2); if (IsGlyphAllowed(tag.type) && ActionElementMap.TryGetCombinedElementIdentifierGlyph(_tempAems, out glyph) && ActionElementMap.TryGetCombinedElementIdentifierFinalGlyphKey(_tempAems, out glyphKey)) { r.glyphKey = glyphKey; r.sprite = glyph as UnityEngine.Sprite; results.Add(r); return true; } else if (IsTextAllowed(tag.type) && ActionElementMap.TryGetCombinedElementIdentifierName(_tempAems, out name)) { r.name = name; results.Add(r); return true; } } bool result = false; // Positive and negative bindings _tempGlyphs.Clear(); _tempKeys.Clear(); result |= TryGetGlyphsOrText(aemResult1, tag.type, _tempGlyphs, _tempKeys, results); _tempGlyphs.Clear(); _tempKeys.Clear(); result |= TryGetGlyphsOrText(aemResult2, tag.type, _tempGlyphs, _tempKeys, results); return result; } private bool TryGetActionDisplayName(ActionTag tag, out string result) { if (tag == null) { result = null; return false; } InputAction action = ReInput.mapping.GetAction(tag.actionId); if (action == null) { result = null; return false; } result = action.GetDisplayName(tag.actionRange); tag.displayName = result; return true; } private bool TryGetPlayerDisplayName(PlayerTag tag, out string result) { if (tag == null) { result = null; return false; } var player = ReInput.players.GetPlayer(tag.playerId); if (player == null) { result = null; return false; } result = player.descriptiveName; tag.displayName = result; return true; } private bool TryAssignSprite(UnityEngine.Sprite sprite, string key) { Asset asset = GetOrCreateAsset(sprite); if (asset == null) return false; ITMProSpriteAsset spriteAsset = asset.spriteAsset; if (!spriteAsset.Contains(key)) { UnityEngine.Rect rect = sprite.rect; ITMProSprite tmpSprite = TMProAssetVersionHelper.CreateSprite(); // id is set in AddSprite tmpSprite.width = rect.width; tmpSprite.height = rect.height; tmpSprite.position = new UnityEngine.Vector2(rect.x, rect.y); tmpSprite.xOffset = rect.width * _spriteOptions.offsetSizeMultiplier.x + _spriteOptions.extraOffset.x; tmpSprite.yOffset = rect.height * _spriteOptions.offsetSizeMultiplier.y + _spriteOptions.extraOffset.y; tmpSprite.xAdvance = rect.width * _spriteOptions.xAdvanceWidthMultiplier + _spriteOptions.extraXAdvance; tmpSprite.scale = _spriteOptions.scale; tmpSprite.pivot = new UnityEngine.Vector2( rect.width * -0.5f, rect.height * 0.5f ); tmpSprite.name = key; tmpSprite.hashCode = TMPro.TMP_TextUtilities.GetSimpleHashCode(key); tmpSprite.sprite = sprite; spriteAsset.AddSprite(tmpSprite); SetDirty(asset); } // Add to currently used assets list if (!_currentlyUsedAssets.Contains(asset)) { _currentlyUsedAssets.Add(asset); } return true; } private void RequireRebuild() { _rebuildRequired = true; } #region Sprite Assets private void CreatePrimaryAsset() { if (_primaryAsset != null) return; _primaryAsset = new Asset(null); // always create with default material, not user-defined base material _tmProText.spriteAsset = _primaryAsset.spriteAsset.GetSpriteAsset(); } private Asset GetOrCreateAsset(UnityEngine.Sprite sprite) { if (sprite == null || sprite.texture == null) return null; // Search assigned list first { int count = _assignedAssets.Count; for (int i = 0; i < count; i++) { if (_assignedAssets[i] == null) continue; if (_assignedAssets[i].spriteAsset.spriteSheet == sprite.texture) { return _assignedAssets[i]; } } } Asset asset = null; // Not found // Check pool for an unused asset { int count = _assetsPool.Count; for (int i = 0; i < count; i++) { if (_assetsPool[i] == null) continue; asset = _assetsPool[i]; _assetsPool.RemoveAt(i); // remove from pool break; } } if (asset == null) { // No asset found. Create one. asset = CreateAsset(); } // Set texture in Sprite Asset and Material asset.spriteAsset.spriteSheet = sprite.texture; asset.material.SetTexture(TMPro.ShaderUtilities.ID_MainTex, sprite.texture); // Assign the Sprite Asset to the primary as a fallback List fallbacks = _primaryAsset.spriteAsset.GetSpriteAsset().fallbackSpriteAssets; if (fallbacks == null) { fallbacks = new List(); _primaryAsset.spriteAsset.GetSpriteAsset().fallbackSpriteAssets = fallbacks; } fallbacks.Add(asset.spriteAsset.GetSpriteAsset()); _assignedAssets.Add(asset); return asset; } private Asset CreateAsset() { Asset asset = new Asset(_baseSpriteMaterial); if (_overrideSpriteMaterialProperties) { CopySpriteMaterialPropertiesToMaterial(_spriteMaterialProperties, asset.material); } return asset; } private void RemoveUnusedAssets() { // Allow a few assets to remain assigned to avoid wasted resources swapping // assets back and forth when changing between last active controllers. const int allowedUnusedAssetCount = 2; // Find assets that are unused and remove them Asset asset; int unusedAssetCount = 0; int count = _assignedAssets.Count; for (int i = count - 1; i >= 0; i--) { // go backwards so newest unused assets are preserved first asset = _assignedAssets[i]; if (asset == null) continue; if (!_currentlyUsedAssets.Contains(asset)) { // not used anymore if (unusedAssetCount >= allowedUnusedAssetCount) { // no more extras allowed // Remove from primary sprite asset fallback list _primaryAsset.spriteAsset.GetSpriteAsset().fallbackSpriteAssets.Remove(asset.spriteAsset.GetSpriteAsset()); // Clean up values when adding to pool asset.spriteAsset.spriteSheet = null; asset.spriteAsset.Clear(); asset.material.SetTexture(TMPro.ShaderUtilities.ID_MainTex, _stubTexture); // set to stub to prevent TMPro from throwing null reference exceptions _assetsPool.Add(asset); // add to pool _assignedAssets.RemoveAt(i); // remove from assigned assets } else { unusedAssetCount += 1; } } } } private void SetDirty(Asset asset) { if (_dirtyAssets.Contains(asset)) return; _dirtyAssets.Add(asset); } private void ForEachAsset(Action callback) { if (callback == null) return; int count; // Set values in all sprites in all assets count = _assignedAssets.Count; for (int i = 0; i < count; i++) { if (_assignedAssets[i] == null) continue; callback(_assignedAssets[i]); } // Material properties need to be set in pool as well count = _assetsPool.Count; for (int i = 0; i < count; i++) { if (_assetsPool[i] == null) continue; callback(_assetsPool[i]); } } #endregion #region Static private static int shaderPropertyId_color { get { return UnityEngine.Shader.PropertyToID("_Color"); } } private static string[] __s_displayTypeNames; private static string[] s_displayTypeNames { get { return __s_displayTypeNames != null ? __s_displayTypeNames : (__s_displayTypeNames = Enum.GetNames(typeof(DisplayType))); } } private static DisplayType[] __s_displayTypeValues; private static DisplayType[] s_displayTypeValues { get { return __s_displayTypeValues != null ? __s_displayTypeValues : (__s_displayTypeValues = (DisplayType[])Enum.GetValues(typeof(DisplayType))); } } private static string[] __s_axisRangeNames; private static string[] s_axisRangeNames { get { return __s_axisRangeNames != null ? __s_axisRangeNames : (__s_axisRangeNames = Enum.GetNames(typeof(AxisRange))); } } private static AxisRange[] __s_axisRangeValues; private static AxisRange[] s_axisRangeValues { get { return __s_axisRangeValues != null ? __s_axisRangeValues : (__s_axisRangeValues = (AxisRange[])Enum.GetValues(typeof(AxisRange))); } } private static void ParseAttributes(string text, int startIndex, int count, StringBuilder sbKey, StringBuilder sbValue, Dictionary results) { if (string.IsNullOrEmpty(text) || startIndex < 0 || startIndex >= text.Length) { return; } results.Clear(); const char doubleQuote = '\"'; const int state_searchForKey = 0; const int state_parsingKey = 1; const int state_searchForValue = 2; const int state_parsingValue = 3; sbKey.Length = 0; sbValue.Length = 0; bool isQuotedValue = true; char c; int lastIndex = startIndex + count - 1; int state = state_searchForKey; try { for (int i = startIndex; i < startIndex + count; i++) { c = text[i]; switch (state) { case state_searchForKey: if (IsValidKeyChar(c)) { state = state_parsingKey; i -= 1; sbKey.Length = 0; } break; case state_parsingKey: if (c == '=') { if (sbKey.Length == 0) throw new Exception("Key was blank."); // no key state = state_searchForValue; } else if (IsValidKeyChar(c)) { sbKey.Append(char.ToLowerInvariant(c)); } else if (char.IsWhiteSpace(c)) { // skip } else { // parsing error throw new Exception("Error parsing key."); } break; case state_searchForValue: { if ((isQuotedValue = c == doubleQuote) || IsValidNonQuotedValueChar(c)) { // allow quoted and non-quoted values if (!isQuotedValue) { i -= 1; } sbValue.Length = 0; state = state_parsingValue; } } break; case state_parsingValue: { if ((isQuotedValue && c == doubleQuote) || (!isQuotedValue && (i == lastIndex || char.IsWhiteSpace(c)))) { // end of value // Append value on last index for non-quoted values, otherwise it will fail if no space before closing tag if (!isQuotedValue && i == lastIndex) sbValue.Append(c); if (sbValue.Length == 0) throw new Exception("Value was blank."); // no value if (results == null) results = new Dictionary(); results.Add(sbKey.ToString(), sbValue.ToString()); state = state_searchForKey; } else { sbValue.Append(c); } } break; } } } catch (Exception ex) { UnityEngine.Debug.LogError(ex); } } private static bool IsValidKeyChar(char c) { return char.IsLetterOrDigit(c) || c == '_'; } private static bool IsValidTagNameChar(char c) { return char.IsLetterOrDigit(c) || c == '_'; } private static bool IsValidNonQuotedValueChar(char c) { return char.IsDigit(c); } private static bool IsEqual(List a, List b) { if (a.Count != b.Count) return false; for (int i = 0; i < a.Count; i++) { if (a[i] != b[i]) return false; } return true; } private static void WriteSpriteKey(StringBuilder sb, string key) { sb.Append(""); } private static bool TryGetGlyphsOrText(ActionElementMap aem, DisplayType displayType, List glyphs, List keys, List results) { if (aem == null || glyphs == null || results == null) return false; GlyphOrText r; if (IsGlyphAllowed(displayType) && aem.GetElementIdentifierGlyphs(glyphs) > 0) { aem.GetElementIdentifierFinalGlyphKeys(keys); if (keys.Count != glyphs.Count) { UnityEngine.Debug.LogError("Rewired: Glyph key count does not match glyph count."); goto TextFallback; } int count = glyphs.Count; for (int i = 0; i < count; i++) { r = new GlyphOrText(); r.glyphKey = keys[i]; r.sprite = glyphs[i]; results.Add(r); } if (count > 0) return true; } TextFallback: if (IsTextAllowed(displayType)) { r = new GlyphOrText(); r.name = aem.elementIdentifierName; results.Add(r); return true; } return false; } private static bool IsGlyphAllowed(DisplayType displayType) { return displayType == DisplayType.Glyph || displayType == DisplayType.GlyphOrText; } private static bool IsTextAllowed(DisplayType displayType) { return displayType == DisplayType.Text || displayType == DisplayType.GlyphOrText; } private static void CopyMaterialProperties(UnityEngine.Material source, UnityEngine.Material destination) { if (source == null || destination == null) return; destination.shader = source.shader; if (source.shaderKeywords != null) { string[] keywords = new string[source.shaderKeywords.Length]; Array.Copy(source.shaderKeywords, keywords, source.shaderKeywords.Length); destination.shaderKeywords = keywords; } else { destination.shaderKeywords = null; } if (source.HasProperty(shaderPropertyId_color) && destination.HasProperty(shaderPropertyId_color)) { destination.color = source.color; } destination.renderQueue = source.renderQueue; destination.globalIlluminationFlags = source.globalIlluminationFlags; } private static void CopySpriteMaterialPropertiesToMaterial(SpriteMaterialProperties properties, UnityEngine.Material material) { if (material == null) return; if (material.HasProperty(shaderPropertyId_color)) { material.color = properties.color; } } #endregion #region Classes private delegate bool ParseTagAttributesHandler(string text, int startIndex, int count, out string replacement); private abstract class Tag { public readonly TagType tagType; private Pool _pool; protected Tag(TagType tagType) { this.tagType = tagType; } protected Pool pool { get { return _pool; } set { _pool = value; } } public void ReturnToPool() { if (_pool == null) return; _pool.Return(this); } protected abstract void Clear(); public static void Clear(List list) { int count = list.Count; for (int i = 0; i < count; i++) { if (list[i] == null) continue; list[i].ReturnToPool(); } list.Clear(); } public enum TagType { ControllerElement, Action, Player } public abstract class Pool { public abstract bool Return(Tag obj); } public sealed class Pool : Pool where T : Tag, new() { private readonly List _list; public Pool() : base() { _list = new List(); } public T Get() { T obj; if (_list.Count == 0) { obj = new T(); if (obj != null) { obj.pool = this; } return obj; } int index = _list.Count - 1; obj = _list[index]; _list.RemoveAt(index); return obj; } public override bool Return(Tag obj) { T tObj = obj as T; if (tObj == null || tObj.pool != this) return false; tObj.Clear(); if (_list.Contains(tObj)) return false; _list.Add(tObj); return true; } } } private sealed class ControllerElementTag : Tag { public DisplayType type; public int playerId; public int actionId; public AxisRange actionRange; private readonly List _glyphsOrText; public List glyphsOrText { get { return _glyphsOrText; } } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append(typeof(ControllerElementTag).Name); sb.Append(": "); sb.Append("type = "); sb.Append(type); sb.Append(", playerId = "); sb.Append(playerId); sb.Append(", actionId = "); sb.Append(actionId); sb.Append(", actionRange = "); sb.Append(actionRange); return sb.ToString(); } public ControllerElementTag() : base(TagType.ControllerElement) { _glyphsOrText = new List(); Clear(); } protected override void Clear() { type = DisplayType.GlyphOrText; playerId = -1; actionId = -1; actionRange = AxisRange.Full; _glyphsOrText.Clear(); } // Static public static bool TryParseString(string text, int startIndex, int count, StringBuilder sb1, StringBuilder sb2, Dictionary workDictionary, Pool pool, out ControllerElementTag result) { result = null; if (string.IsNullOrEmpty(text) || startIndex < 0 || startIndex + count >= text.Length) return false; ParseAttributes(text, startIndex, count, sb1, sb2, workDictionary); if (workDictionary.Count == 0) return false; string value; result = pool.Get(); try { // Type -- optional if (workDictionary.TryGetValue("type", out value)) { bool found = false; for (int i = 0; i < s_displayTypeNames.Length; i++) { if (string.Equals(value, s_displayTypeNames[i], StringComparison.OrdinalIgnoreCase)) { result.type = s_displayTypeValues[i]; found = true; break; } } if (!found) throw new Exception("Invalid type: " + value); } else { result.type = DisplayType.GlyphOrText; // default } // Player name or id if (workDictionary.TryGetValue("playerid", out value)) { result.playerId = int.Parse(value); if (ReInput.players.GetPlayer(result.playerId) == null) throw new Exception("Invalid Player Id: " + result.playerId); } else if (workDictionary.TryGetValue("playername", out value)) { var player = ReInput.players.GetPlayer(value); if (player == null) throw new Exception("Invalid Player name: " + value); result.playerId = player.id; } else { // error throw new Exception("Player name/id missing."); } // Action name or id if (workDictionary.TryGetValue("actionid", out value)) { result.actionId = int.Parse(value); if (ReInput.mapping.GetAction(result.actionId) == null) throw new Exception("Invalid Action Id: " + result.actionId); } else if (workDictionary.TryGetValue("actionname", out value)) { var action = ReInput.mapping.GetAction(value); if (action == null) throw new Exception("Invalid Action name: " + value); result.actionId = action.id; } else { // error throw new Exception("Action name/id missing."); } // Action Range -- optional if (workDictionary.TryGetValue("actionrange", out value)) { bool found = false; for (int i = 0; i < s_axisRangeNames.Length; i++) { if (string.Equals(value, s_axisRangeNames[i], StringComparison.OrdinalIgnoreCase)) { result.actionRange = s_axisRangeValues[i]; found = true; break; } } if (!found) throw new Exception("Invalid Action Range: " + value); } else { result.actionRange = AxisRange.Full; } return true; } catch (Exception ex) { UnityEngine.Debug.LogError(ex); result.ReturnToPool(); return false; } } } private sealed class ActionTag : Tag { public int actionId; public AxisRange actionRange; private string _displayName; public string displayName { get { return _displayName; } set { _displayName = value; } } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append(typeof(ControllerElementTag).Name); sb.Append(": "); sb.Append("actionId = "); sb.Append(actionId); sb.Append(", actionRange = "); sb.Append(actionRange); return sb.ToString(); } public ActionTag() : base(TagType.Action) { Clear(); } protected override void Clear() { actionId = -1; actionRange = AxisRange.Full; _displayName = null; } // Static public static bool TryParseString(string text, int startIndex, int count, StringBuilder sb1, StringBuilder sb2, Dictionary workDictionary, Pool pool, out ActionTag result) { result = null; if (string.IsNullOrEmpty(text) || startIndex < 0 || startIndex + count >= text.Length) return false; ParseAttributes(text, startIndex, count, sb1, sb2, workDictionary); if (workDictionary.Count == 0) return false; string value; result = pool.Get(); try { // Action name or id if (workDictionary.TryGetValue("id", out value) || workDictionary.TryGetValue("actionid", out value)) { result.actionId = int.Parse(value); if (ReInput.mapping.GetAction(result.actionId) == null) throw new Exception("Invalid Action Id: " + result.actionId); } else if (workDictionary.TryGetValue("name", out value) || workDictionary.TryGetValue("actionname", out value)) { var action = ReInput.mapping.GetAction(value); if (action == null) throw new Exception("Invalid Action name: " + value); result.actionId = action.id; } else { // error throw new Exception("Action name/id missing."); } // Action Range -- optional if (workDictionary.TryGetValue("range", out value) || workDictionary.TryGetValue("actionrange", out value)) { bool found = false; for (int i = 0; i < s_axisRangeNames.Length; i++) { if (string.Equals(value, s_axisRangeNames[i], StringComparison.OrdinalIgnoreCase)) { result.actionRange = s_axisRangeValues[i]; found = true; break; } } if (!found) throw new Exception("Invalid Action Range: " + value); } else { result.actionRange = AxisRange.Full; } return true; } catch (Exception ex) { UnityEngine.Debug.LogError(ex); result.ReturnToPool(); return false; } } } private sealed class PlayerTag : Tag { public int playerId; private string _displayName; public string displayName { get { return _displayName; } set { _displayName = value; } } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append(typeof(ControllerElementTag).Name); sb.Append(": "); sb.Append("playerId = "); sb.Append(playerId); return sb.ToString(); } public PlayerTag() : base(TagType.Player) { Clear(); } protected override void Clear() { playerId = -1; _displayName = null; } // Static public static bool TryParseString(string text, int startIndex, int count, StringBuilder sb1, StringBuilder sb2, Dictionary workDictionary, Pool pool, out PlayerTag result) { result = null; if (string.IsNullOrEmpty(text) || startIndex < 0 || startIndex + count >= text.Length) return false; ParseAttributes(text, startIndex, count, sb1, sb2, workDictionary); if (workDictionary.Count == 0) return false; string value; result = pool.Get(); try { // Action name or id if (workDictionary.TryGetValue("id", out value) || workDictionary.TryGetValue("playerid", out value)) { result.playerId = int.Parse(value); if (ReInput.players.GetPlayer(result.playerId) == null) throw new Exception("Invalid Player Id: " + result.playerId); } else if (workDictionary.TryGetValue("name", out value) || workDictionary.TryGetValue("playername", out value)) { var player = ReInput.players.GetPlayer(value); if (player == null) throw new Exception("Invalid Player name: " + value); result.playerId = player.id; } else { // error throw new Exception("Player name/id missing."); } return true; } catch (Exception ex) { UnityEngine.Debug.LogError(ex); result.ReturnToPool(); return false; } } } private struct GlyphOrText : IEquatable { public string glyphKey; public UnityEngine.Sprite sprite; public string name; public override bool Equals(object obj) { if (!(obj is GlyphOrText)) return false; GlyphOrText item = (GlyphOrText)obj; return string.Equals(item.glyphKey, glyphKey, StringComparison.Ordinal) && item.sprite == sprite && string.Equals(item.name, name, StringComparison.Ordinal); } public override int GetHashCode() { int hash = 17; hash = hash * 29 + glyphKey.GetHashCode(); hash = hash * 29 + sprite.GetHashCode(); hash = hash * 29 + name.GetHashCode(); return hash; } public bool Equals(GlyphOrText other) { return string.Equals(other.glyphKey, glyphKey, StringComparison.Ordinal) && other.sprite == sprite && string.Equals(other.name, name, StringComparison.Ordinal); } public static bool operator ==(GlyphOrText a, GlyphOrText b) { return string.Equals(a.glyphKey, b.glyphKey, StringComparison.Ordinal) && a.sprite == b.sprite && string.Equals(a.name, b.name, StringComparison.Ordinal); } public static bool operator !=(GlyphOrText a, GlyphOrText b) { return !(a == b); } } private class Asset { public readonly uint id; private ITMProSpriteAsset _spriteAsset; private UnityEngine.Material _material; public ITMProSpriteAsset spriteAsset { get { return _spriteAsset; } } public UnityEngine.Material material { get { return _material; } } public Asset(UnityEngine.Material baseMaterial) { id = s_idCounter++; _spriteAsset = TMProAssetVersionHelper.CreateSpriteAsset(); TMPro.TMP_SpriteAsset spriteAsset = _spriteAsset.GetSpriteAsset(); spriteAsset.name = typeof(UnityUITextMeshProGlyphHelper).Name + " SpriteAsset " + id; spriteAsset.hashCode = TMPro.TMP_TextUtilities.GetSimpleHashCode(spriteAsset.name); _material = CreateMaterial(baseMaterial, id); if (_spriteAsset != null) { spriteAsset.material = material; spriteAsset.materialHashCode = TMPro.TMP_TextUtilities.GetSimpleHashCode(material.name); } } public static UnityEngine.Material CreateMaterial(UnityEngine.Material baseMaterial, uint id) { UnityEngine.Material material; material = baseMaterial != null ? new UnityEngine.Material(baseMaterial) : new UnityEngine.Material(tmProShader); material.name = typeof(UnityUITextMeshProGlyphHelper).Name + " Material " + id; material.hideFlags = UnityEngine.HideFlags.HideInHierarchy; return material; } public void Destroy() { if (_spriteAsset != null) { _spriteAsset.Destroy(); _spriteAsset = null; } if (_material != null) { UnityEngine.Object.Destroy(_material); _material = null; } } // Static private static uint s_idCounter; private static UnityEngine.Shader __tmProShader; private static UnityEngine.Shader tmProShader { get { if (__tmProShader == null) { TMPro.ShaderUtilities.GetShaderPropertyIDs(); __tmProShader = UnityEngine.Shader.Find("TextMeshPro/Sprite"); } return __tmProShader; } } } /// /// Options for TMPro Sprite rendering. /// [Serializable] public struct TMProSpriteOptions : IEquatable { [UnityEngine.Tooltip("Scale.")] [UnityEngine.SerializeField] private float _scale; [UnityEngine.Tooltip("This value will be multiplied by the Sprite width and height and applied to offset.")] [UnityEngine.SerializeField] private UnityEngine.Vector2 _offsetSizeMultiplier; [UnityEngine.Tooltip("An extra offset that is cumulative with Offset Size Multiplier.")] [UnityEngine.SerializeField] private UnityEngine.Vector2 _extraOffset; [UnityEngine.Tooltip("This value will be multiplied by the Sprite width applied to X Advance.")] [UnityEngine.SerializeField] private float _xAdvanceWidthMultiplier; [UnityEngine.Tooltip("An extra offset that is cumulative with X Advance Width Multiplier.")] [UnityEngine.SerializeField] private float _extraXAdvance; /// /// Sprites will be scaled by this value. /// public float scale { get { return _scale; } set { _scale = value; } } /// /// This value will be multiplied by the Sprite width and height and applied to offset. /// public UnityEngine.Vector2 offsetSizeMultiplier { get { return _offsetSizeMultiplier; } set { _offsetSizeMultiplier = value; } } /// /// An extra offset that is cumulative with . /// public UnityEngine.Vector2 extraOffset { get { return _extraOffset; } set { _extraOffset = value; } } /// /// This value will be multiplied by the Sprite width applied to X Advance. /// public float xAdvanceWidthMultiplier { get { return _xAdvanceWidthMultiplier; } set { _xAdvanceWidthMultiplier = value; } } /// /// An extra offset that is cumulative with . /// public float extraXAdvance { get { return _extraXAdvance; } set { _extraXAdvance = value; } } /// /// Creates an instance with the default values. /// /// An instance with the default values. public static TMProSpriteOptions Default { get { TMProSpriteOptions o = new TMProSpriteOptions(); o.scale = 1.5f; o.extraOffset = new UnityEngine.Vector2(); o.offsetSizeMultiplier = new UnityEngine.Vector2(0f, 0.75f); o.xAdvanceWidthMultiplier = 1f; return o; } } public override bool Equals(object obj) { if (!(obj is TMProSpriteOptions)) return false; TMProSpriteOptions item = (TMProSpriteOptions)obj; return item._scale == _scale && item._offsetSizeMultiplier == _offsetSizeMultiplier && item._extraOffset == _extraOffset && item._xAdvanceWidthMultiplier == _xAdvanceWidthMultiplier && item._extraXAdvance == _extraXAdvance; } public override int GetHashCode() { int hash = 17; hash = hash * 29 + _scale.GetHashCode(); hash = hash * 29 + _offsetSizeMultiplier.GetHashCode(); hash = hash * 29 + _extraOffset.GetHashCode(); hash = hash * 29 + _xAdvanceWidthMultiplier.GetHashCode(); hash = hash * 29 + _extraXAdvance.GetHashCode(); return hash; } public bool Equals(TMProSpriteOptions other) { return other._scale == _scale && other._offsetSizeMultiplier == _offsetSizeMultiplier && other._extraOffset == _extraOffset && other._xAdvanceWidthMultiplier == _xAdvanceWidthMultiplier && other._extraXAdvance == _extraXAdvance; } public static bool operator ==(TMProSpriteOptions a, TMProSpriteOptions b) { return a._scale == b._scale && a._offsetSizeMultiplier == b._offsetSizeMultiplier && a._extraOffset == b._extraOffset && a._xAdvanceWidthMultiplier == b._xAdvanceWidthMultiplier && a._extraXAdvance == b._extraXAdvance; } public static bool operator !=(TMProSpriteOptions a, TMProSpriteOptions b) { return !(a == b); } } /// /// Sprite material properties. /// [Serializable] public struct SpriteMaterialProperties { [UnityEngine.Tooltip("Sprite material color.")] [UnityEngine.SerializeField] private UnityEngine.Color _color; public UnityEngine.Color color { get { return _color; } set { _color = value; } } public static SpriteMaterialProperties Default { get { SpriteMaterialProperties o = new SpriteMaterialProperties(); o._color = UnityEngine.Color.white; return o; } } } #region Text Mesh Pro Asset Version Support // These wrapper classes are required to support both Unity UI 1.0 and // Unity UI 2.0, or mores specifically, versions of Text Mesh Pro pre // and post asset v1.1.0 breaking structural changes were made. private interface ITMProSprite { uint id { get; set; } float width { get; set; } float height { get; set; } float xOffset { get; set; } float yOffset { get; set; } float xAdvance { get; set; } UnityEngine.Vector2 position { get; set; } UnityEngine.Vector2 pivot { get; set; } float scale { get; set; } string name { get; set; } uint unicode { get; set; } int hashCode { get; set; } UnityEngine.Sprite sprite { get; set; } } private interface ITMProSpriteAsset { int spriteCount { get; } UnityEngine.Texture spriteSheet { get; set; } TMPro.TMP_SpriteAsset GetSpriteAsset(); ITMProSprite GetSprite(int index); void AddSprite(ITMProSprite sprite); bool Contains(string spriteName); void Clear(); void UpdateLookupTables(); void Destroy(); } private static class TMProAssetVersionHelper { #if MAY_SUPPORT_TMPRO_ASSET_V_1_1_0 private static bool _isVersionSupportedChecked; private static bool CheckVersionSupported() { bool isVersionSupported = TMProSprite_AssetV1_1_0.CheckVersionSupported(); if (_isVersionSupportedChecked) return isVersionSupported; if (!isVersionSupported) { #if UNITY_EDITOR //UnityEngine.Debug.LogWarning(typeof(UnityUITextMeshProGlyphHelper).Name + ": Unity UI 2.0 is not supported."); #endif } _isVersionSupportedChecked = true; return isVersionSupported; } #endif public static ITMProSprite CreateSprite() { #if MAY_SUPPORT_TMPRO_ASSET_V_1_1_0 return CheckVersionSupported() ? (ITMProSprite)new TMProSprite_AssetV1_1_0() : (ITMProSprite)new TMProSprite_AssetV1_0_0(); #else return new TMProSprite_AssetV1_0_0(); #endif } public static ITMProSpriteAsset CreateSpriteAsset() { #if MAY_SUPPORT_TMPRO_ASSET_V_1_1_0 return CheckVersionSupported() ? (ITMProSpriteAsset)new TMProSprite_AssetV1_1_0.TMPro_SpriteAsset() : (ITMProSpriteAsset)new TMProSprite_AssetV1_0_0.TMPro_SpriteAsset(); #else return new TMProSprite_AssetV1_0_0.TMPro_SpriteAsset(); #endif } } private class TMProSprite_AssetV1_0_0 : ITMProSprite { public TMPro.TMP_Sprite spriteInfo; public TMProSprite_AssetV1_0_0() { spriteInfo = new TMPro.TMP_Sprite(); } public uint id { get { return (uint)spriteInfo.id; } set { spriteInfo.id = (int)value; } } public float width { get { return spriteInfo.width; } set { spriteInfo.width = value; } } public float height { get { return spriteInfo.height; } set { spriteInfo.height = value; } } public float xOffset { get { return spriteInfo.xOffset; } set { spriteInfo.xOffset = value; } } public float yOffset { get { return spriteInfo.yOffset; } set { spriteInfo.yOffset = value; } } public float xAdvance { get { return spriteInfo.xAdvance; } set { spriteInfo.xAdvance = value; } } public UnityEngine.Vector2 position { get { return new UnityEngine.Vector2(spriteInfo.x, spriteInfo.y); } set { spriteInfo.x = value.x; spriteInfo.y = value.y; } } public UnityEngine.Vector2 pivot { get { return spriteInfo.pivot; } set { spriteInfo.pivot = value; } } public float scale { get { return spriteInfo.scale; } set { spriteInfo.scale = value; } } public string name { get { return spriteInfo.name; } set { spriteInfo.name = value; } } public uint unicode { get { return (uint)spriteInfo.unicode; } set { spriteInfo.unicode = (int)value; } } public int hashCode { get { return spriteInfo.hashCode; } set { spriteInfo.hashCode = value; } } public UnityEngine.Sprite sprite { get { return spriteInfo.sprite; } set { spriteInfo.sprite = value; } } public class TMPro_SpriteAsset : ITMProSpriteAsset { private TMPro.TMP_SpriteAsset _spriteAsset; private readonly List _sprites; public int spriteCount { get { return _sprites.Count; } } public UnityEngine.Texture spriteSheet { get { return _spriteAsset.spriteSheet; } set { _spriteAsset.spriteSheet = value; } } public TMPro_SpriteAsset() { _spriteAsset = UnityEngine.ScriptableObject.CreateInstance(); _spriteAsset.hideFlags = UnityEngine.HideFlags.DontSave; if (_spriteAsset.spriteInfoList == null) _spriteAsset.spriteInfoList = new List(); _sprites = new List(); } public TMPro.TMP_SpriteAsset GetSpriteAsset() { return _spriteAsset; } public ITMProSprite GetSprite(int index) { if ((uint)index >= (uint)_sprites.Count) return null; return _sprites[index]; } public void AddSprite(ITMProSprite sprite) { TMProSprite_AssetV1_0_0 tSprite = sprite as TMProSprite_AssetV1_0_0; if (sprite == null) throw new ArgumentException(); tSprite.spriteInfo.id = _spriteAsset.spriteInfoList.Count; _spriteAsset.spriteInfoList.Add(tSprite.spriteInfo); _sprites.Add(tSprite); } public void Clear() { _spriteAsset.spriteInfoList.Clear(); _sprites.Clear(); } public bool Contains(string spriteName) { int count = _sprites.Count; for (int i = 0; i < count; i++) { if (string.Equals(_sprites[i].name, spriteName, StringComparison.Ordinal)) return true; } return false; } public void UpdateLookupTables() { _spriteAsset.UpdateLookupTables(); } public void Destroy() { if (_spriteAsset == null) return; UnityEngine.Object.Destroy(_spriteAsset); _spriteAsset = null; } } } #if MAY_SUPPORT_TMPRO_ASSET_V_1_1_0 private class TMProSprite_AssetV1_1_0 : ITMProSprite { private readonly TMPro_SpriteGlyph _spriteGlyph; private readonly TMPro_SpriteCharacter _spriteCharacter; public TMProSprite_AssetV1_1_0() { _spriteGlyph = new TMPro_SpriteGlyph(); _spriteCharacter = new TMPro_SpriteCharacter(); _spriteCharacter.glyph = _spriteGlyph.source; } public TMPro_SpriteGlyph spriteGlyph { get { return _spriteGlyph; } } public TMPro_SpriteCharacter spriteCharacter { get { return _spriteCharacter; } } public uint id { get { return _spriteGlyph.source.index; } set { _spriteGlyph.source.index = value; _spriteCharacter.glyphIndex = value; } } public float width { get { return _spriteGlyph.source.metrics.width; } set { UnityEngine.TextCore.GlyphMetrics m = _spriteGlyph.source.metrics; m.width = value; _spriteGlyph.source.metrics = m; UnityEngine.TextCore.GlyphRect r = _spriteGlyph.source.glyphRect; r.width = (int)value; _spriteGlyph.source.glyphRect = r; } } public float height { get { return _spriteGlyph.source.metrics.height; } set { UnityEngine.TextCore.GlyphMetrics m = _spriteGlyph.source.metrics; m.height = value; _spriteGlyph.source.metrics = m; UnityEngine.TextCore.GlyphRect r = _spriteGlyph.source.glyphRect; r.height = (int)value; _spriteGlyph.source.glyphRect = r; } } public float xOffset { get { return _spriteGlyph.source.metrics.horizontalBearingX; } set { UnityEngine.TextCore.GlyphMetrics m = _spriteGlyph.source.metrics; m.horizontalBearingX = value; _spriteGlyph.source.metrics = m; } } public float yOffset { get { return _spriteGlyph.source.metrics.horizontalBearingY; } set { UnityEngine.TextCore.GlyphMetrics m = _spriteGlyph.source.metrics; m.horizontalBearingY = value; _spriteGlyph.source.metrics = m; } } public float xAdvance { get { return _spriteGlyph.source.metrics.horizontalAdvance; } set { UnityEngine.TextCore.GlyphMetrics m = _spriteGlyph.source.metrics; m.horizontalAdvance = value; _spriteGlyph.source.metrics = m; } } public UnityEngine.Vector2 position { get { var rect = _spriteGlyph.source.glyphRect; return new UnityEngine.Vector2(rect.x, rect.y); } set { UnityEngine.TextCore.GlyphRect r = _spriteGlyph.source.glyphRect; r.x = (int)value.x; r.y = (int)value.y; _spriteGlyph.source.glyphRect = r; } } public UnityEngine.Vector2 pivot { get { return new UnityEngine.Vector2(); } set { } } public float scale { get { return _spriteCharacter.scale; } set { _spriteCharacter.scale = value; } } public string name { get { return _spriteCharacter.name; } set { _spriteCharacter.name = value; } } public uint unicode { get { return _spriteCharacter.unicode; } set { _spriteCharacter.unicode = value; } } public int hashCode { get { return 0; } set { } } public UnityEngine.Sprite sprite { get { return _spriteGlyph.sprite; } set { _spriteGlyph.sprite = value; } } // Static private static bool? s_isVersionSupported; public static bool CheckVersionSupported() { if (s_isVersionSupported != null) return s_isVersionSupported.Value; try { #if !SUPPORTS_AT_LEAST_TMPRO_ASSET_V_1_1_0 var a = new TMPro_SpriteCharacter(); var b = new TMPro_SpriteGlyph(); #endif var c = new TMPro_SpriteAsset(); s_isVersionSupported = true; } catch { s_isVersionSupported = false; } return s_isVersionSupported.Value; } // Classes public class TMPro_SpriteCharacter { #if SUPPORTS_AT_LEAST_TMPRO_ASSET_V_1_1_0 private readonly TMPro.TMP_SpriteCharacter _source; public TMPro.TMP_SpriteCharacter source { get { return _source; } } public UnityEngine.TextCore.Glyph glyph { get { return _source.glyph; } set { _source.glyph = value; } } public uint unicode { get { return _source.unicode; } set { if (value == 0x0) value = 0xFFFE; _source.unicode = value; } } public string name { get { return _source.name; } set { _source.name = value; } } public float scale { get { return _source.scale; } set { _source.scale = value; } } public uint glyphIndex { get { return _source.glyphIndex; } set { _source.glyphIndex = value; } } public TMPro_SpriteCharacter() { _source = new TMPro.TMP_SpriteCharacter(); } #else private const string typeFullName = "TMPro.TMP_SpriteCharacter"; private readonly object _source; private readonly PropertyInfo _glyph; private readonly PropertyInfo _unicode; private readonly PropertyInfo _name; private readonly PropertyInfo _scale; private readonly PropertyInfo _glyphIndex; public object source { get { return _source; } } public UnityEngine.TextCore.Glyph glyph { get { return (UnityEngine.TextCore.Glyph)_glyph.GetValue(_source); } set { _glyph.SetValue(_source, value); } } public uint unicode { get { return (uint)_unicode.GetValue(_source); } set { if (value == 0x0) value = 0xFFFE; _unicode.SetValue(_source, value); } } public string name { get { return (string)_name.GetValue(_source); } set { _name.SetValue(_source, value); } } public float scale { get { return (float)_scale.GetValue(_source); } set { _scale.SetValue(_source, value); } } public uint glyphIndex { get { return (uint)_glyphIndex.GetValue(_source); } set { _glyphIndex.SetValue(_source, value); } } public TMPro_SpriteCharacter() { System.Type type = GetReflectedType(); if (type == null) throw new ArgumentNullException("type"); _source = System.Activator.CreateInstance(type); if (_source == null) throw new ArgumentNullException("source"); _glyph = type.GetProperty("glyph", BindingFlags.Public | BindingFlags.Instance); if (_glyph == null) throw new ArgumentNullException("glyph"); _unicode = type.GetProperty("unicode", BindingFlags.Public | BindingFlags.Instance); if (_unicode == null) throw new ArgumentNullException("unicode"); _name = type.GetProperty("name", BindingFlags.Public | BindingFlags.Instance); if (_name == null) throw new ArgumentNullException("name"); _scale = type.GetProperty("scale", BindingFlags.Public | BindingFlags.Instance); if (_scale == null) throw new ArgumentNullException("scale"); _glyphIndex = type.GetProperty("glyphIndex", BindingFlags.Public | BindingFlags.Instance); if (_glyphIndex == null) throw new ArgumentNullException("glyphIndex"); } // Static private static System.Type s_type; private static System.Type GetReflectedType() { if (s_type != null) return s_type; System.Type[] types = typeof(TMPro.TMP_SpriteAsset).Assembly.GetTypes(); if (types == null) return null; for(int i = 0; i < types.Length; i++) { if (string.Equals(types[i].FullName, typeFullName)) { s_type = types[i]; break; } } return s_type; } #endif } public class TMPro_SpriteGlyph { #if SUPPORTS_AT_LEAST_TMPRO_ASSET_V_1_1_0 private readonly TMPro.TMP_SpriteGlyph _source; public TMPro.TMP_SpriteGlyph source { get { return _source; } } public UnityEngine.Sprite sprite { get { return _source.sprite; } set { _source.sprite = value; } } public TMPro_SpriteGlyph() { _source = new TMPro.TMP_SpriteGlyph(); Initialize(_source); } #else private const string typeFullName = "TMPro.TMP_SpriteGlyph"; private readonly UnityEngine.TextCore.Glyph _source; private readonly FieldInfo _sprite; public UnityEngine.TextCore.Glyph source { get { return _source; } } public UnityEngine.Sprite sprite { get { return (UnityEngine.Sprite)_sprite.GetValue(_source); } set { _sprite.SetValue(_source, value); } } public TMPro_SpriteGlyph() { System.Type type = GetReflectedType(); if (type == null) throw new ArgumentNullException("type"); _source = (UnityEngine.TextCore.Glyph)System.Activator.CreateInstance(type); if (_source == null) throw new ArgumentNullException("glyph"); _sprite = type.GetField("sprite", BindingFlags.Public | BindingFlags.Instance); if (_sprite == null) throw new ArgumentNullException("sprite"); Initialize(_source); } // Static private static System.Type s_type; private static System.Type GetReflectedType() { if (s_type != null) return s_type; System.Type[] types = typeof(TMPro.TMP_SpriteAsset).Assembly.GetTypes(); if (types == null) return null; for (int i = 0; i < types.Length; i++) { if (string.Equals(types[i].FullName, typeFullName)) { s_type = types[i]; break; } } return s_type; } #endif private static void Initialize(UnityEngine.TextCore.Glyph glyph) { glyph.scale = 1.0f; glyph.atlasIndex = 0; } } public class TMPro_SpriteAsset : ITMProSpriteAsset { // Cannot avoid using reflection in this class because TMPro.TMP_SpriteAsset // has internal setters for the tables and version in Unity UI 2.0. This // effectively prevents scripts from modfying the Sprite Asset tables at runtime. // This needs be changed back in a future version of Unity UI / TMPro. private readonly PropertyInfo _spriteCharacterTable; private readonly PropertyInfo _spriteGlyphTable; private readonly System.Collections.IList _spriteCharacterTableList; private readonly System.Collections.IList _spriteGlyphTableList; private readonly List _sprites; private TMPro.TMP_SpriteAsset _spriteAsset; public int spriteCount { get { return _sprites.Count; } } public UnityEngine.Texture spriteSheet { get { return _spriteAsset.spriteSheet; } set { _spriteAsset.spriteSheet = value; } } public TMPro_SpriteAsset() { _spriteAsset = UnityEngine.ScriptableObject.CreateInstance(); _spriteAsset.hideFlags = UnityEngine.HideFlags.DontSave; Type type = typeof(TMPro.TMP_SpriteAsset); if (type == null) throw new ArgumentNullException("type"); // Set version to prevent TMPro asset version upgrade logging PropertyInfo version = type.GetProperty("version", BindingFlags.Public | BindingFlags.Instance); if (version == null) throw new ArgumentNullException("version"); version.SetValue(_spriteAsset, "1.1.0"); _spriteCharacterTable = type.GetProperty("spriteCharacterTable", BindingFlags.Public | BindingFlags.Instance); if (_spriteCharacterTable == null) throw new ArgumentNullException("spriteCharacterTable"); _spriteCharacterTableList = (System.Collections.IList)_spriteCharacterTable.GetValue(_spriteAsset); if (_spriteCharacterTableList == null) throw new ArgumentNullException("spriteCharacterTableList"); _spriteGlyphTable = type.GetProperty("spriteGlyphTable", BindingFlags.Public | BindingFlags.Instance); if (_spriteGlyphTable == null) throw new ArgumentNullException("spriteGlyphTable"); _spriteGlyphTableList = (System.Collections.IList)_spriteGlyphTable.GetValue(_spriteAsset); if (_spriteGlyphTableList == null) throw new ArgumentNullException("spriteGlyphTableList"); _sprites = new List(); } public TMPro.TMP_SpriteAsset GetSpriteAsset() { return _spriteAsset; } public ITMProSprite GetSprite(int index) { if ((uint)index >= (uint)_sprites.Count) return null; return _sprites[index]; } public void AddSprite(ITMProSprite sprite) { TMProSprite_AssetV1_1_0 tSprite = sprite as TMProSprite_AssetV1_1_0; if (tSprite == null) throw new ArgumentException(); tSprite.id = (uint)_spriteCharacterTableList.Count; _spriteCharacterTableList.Add(tSprite.spriteCharacter.source); _spriteGlyphTableList.Add(tSprite.spriteGlyph.source); _sprites.Add(tSprite); } public void Clear() { _spriteCharacterTableList.Clear(); _spriteGlyphTableList.Clear(); _sprites.Clear(); } public bool Contains(string spriteName) { int count = _sprites.Count; for (int i = 0; i < count; i++) { if (string.Equals(_sprites[i].name, spriteName, StringComparison.Ordinal)) return true; } return false; } public void UpdateLookupTables() { _spriteAsset.UpdateLookupTables(); } public void Destroy() { if (_spriteAsset == null) return; UnityEngine.Object.Destroy(_spriteAsset); _spriteAsset = null; } } } #endif #endregion #endregion #region Enums private enum DisplayType { Glyph, Text, GlyphOrText } #endregion } } #endif