A game framework written with osu! in mind.
at master 14 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.ComponentModel; 7using System.Drawing; 8using System.IO; 9using System.Linq; 10using System.Reflection; 11using System.Security.Cryptography; 12using System.Text; 13using osu.Framework.Extensions.ObjectExtensions; 14using osu.Framework.Localisation; 15using osu.Framework.Platform; 16using osuTK; 17 18// this is an abusive thing to do, but it increases the visibility of Extension Methods to virtually every file. 19 20namespace osu.Framework.Extensions 21{ 22 /// <summary> 23 /// This class holds extension methods for various purposes and should not be used explicitly, ever. 24 /// </summary> 25 public static class ExtensionMethods 26 { 27 /// <summary> 28 /// Adds the given item to the list according to standard sorting rules. Do not use on unsorted lists. 29 /// </summary> 30 /// <param name="list">The list to take values</param> 31 /// <param name="item">The item that should be added.</param> 32 /// <returns>The index in the list where the item was inserted.</returns> 33 public static int AddInPlace<T>(this List<T> list, T item) 34 { 35 int index = list.BinarySearch(item); 36 if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. 37 list.Insert(index, item); 38 return index; 39 } 40 41 /// <summary> 42 /// Adds the given item to the list according to the comparers sorting rules. Do not use on unsorted lists. 43 /// </summary> 44 /// <param name="list">The list to take values</param> 45 /// <param name="item">The item that should be added.</param> 46 /// <param name="comparer">The comparer that should be used for sorting.</param> 47 /// <returns>The index in the list where the item was inserted.</returns> 48 public static int AddInPlace<T>(this List<T> list, T item, IComparer<T> comparer) 49 { 50 int index = list.BinarySearch(item, comparer); 51 if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement. 52 list.Insert(index, item); 53 return index; 54 } 55 56 /// <summary> 57 /// Try to get a value from the <paramref name="dictionary"/>. Returns a default(TValue) if the key does not exist. 58 /// </summary> 59 /// <param name="dictionary">The dictionary.</param> 60 /// <param name="lookup">The lookup key.</param> 61 [Obsolete("Use System.Collections.Generic.CollectionExtensions.GetValueOrDefault instead.")] // Can be removed 20220115 62 public static TValue GetOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey lookup) => dictionary.GetValueOrDefault(lookup); 63 64 /// <summary> 65 /// Converts a rectangular array to a jagged array. 66 /// <para> 67 /// The jagged array will contain empty arrays if there are no columns in the rectangular array. 68 /// </para> 69 /// </summary> 70 /// <param name="rectangular">The rectangular array.</param> 71 /// <returns>The jagged array.</returns> 72 public static T[][] ToJagged<T>(this T[,] rectangular) 73 { 74 if (rectangular == null) 75 return null; 76 77 var jagged = new T[rectangular.GetLength(0)][]; 78 79 for (int r = 0; r < rectangular.GetLength(0); r++) 80 { 81 jagged[r] = new T[rectangular.GetLength(1)]; 82 for (int c = 0; c < rectangular.GetLength(1); c++) 83 jagged[r][c] = rectangular[r, c]; 84 } 85 86 return jagged; 87 } 88 89 /// <summary> 90 /// Converts a jagged array to a rectangular array. 91 /// <para> 92 /// All elements that did not exist in the original jagged array are initialized to their default values. 93 /// </para> 94 /// </summary> 95 /// <param name="jagged">The jagged array.</param> 96 /// <returns>The rectangular array.</returns> 97 public static T[,] ToRectangular<T>(this T[][] jagged) 98 { 99 if (jagged == null) 100 return null; 101 102 var rows = jagged.Length; 103 var cols = rows == 0 ? 0 : jagged.Max(c => c?.Length ?? 0); 104 105 var rectangular = new T[rows, cols]; 106 107 for (int r = 0; r < rows; r++) 108 { 109 for (int c = 0; c < cols; c++) 110 { 111 if (jagged[r] == null) 112 continue; 113 114 if (c >= jagged[r].Length) 115 continue; 116 117 rectangular[r, c] = jagged[r][c]; 118 } 119 } 120 121 return rectangular; 122 } 123 124 /// <summary> 125 /// Inverts the rows and columns of a rectangular array. 126 /// </summary> 127 /// <param name="array">The array to invert.</param> 128 /// <returns>The inverted array.</returns> 129 public static T[,] Invert<T>(this T[,] array) 130 { 131 if (array == null) 132 return null; 133 134 int rows = array.GetLength(0); 135 int cols = array.GetLength(1); 136 137 var result = new T[cols, rows]; 138 139 for (int r = 0; r < rows; r++) 140 { 141 for (int c = 0; c < cols; c++) 142 result[c, r] = array[r, c]; 143 } 144 145 return result; 146 } 147 148 /// <summary> 149 /// Inverts the rows and columns of a jagged array. 150 /// </summary> 151 /// <param name="array">The array to invert.</param> 152 /// <returns>The inverted array. This is always a square array.</returns> 153 public static T[][] Invert<T>(this T[][] array) => array.ToRectangular().Invert().ToJagged(); 154 155 public static string ToResolutionString(this Size size) => $"{size.Width}x{size.Height}"; 156 157 public static Type[] GetLoadableTypes(this Assembly assembly) 158 { 159 if (assembly == null) throw new ArgumentNullException(nameof(assembly)); 160 161 try 162 { 163 return assembly.GetTypes(); 164 } 165 catch (ReflectionTypeLoadException e) 166 { 167 // the following warning disables are caused by netstandard2.1 and net5.0 differences 168 // the former declares Types as Type[], while the latter declares as Type?[]: 169 // https://docs.microsoft.com/en-us/dotnet/api/system.reflection.reflectiontypeloadexception.types?view=net-5.0#property-value 170 // which trips some inspectcode errors which are only "valid" for the first of the two. 171 // TODO: remove if netstandard2.1 is removed 172 // ReSharper disable once ConditionIsAlwaysTrueOrFalse 173 // ReSharper disable once ConstantConditionalAccessQualifier 174 // ReSharper disable once ConstantNullCoalescingCondition 175 return e.Types?.Where(t => t != null).ToArray() ?? Array.Empty<Type>(); 176 } 177 } 178 179 /// <summary> 180 /// Returns the localisable description of a given object, via (in order): 181 /// <list type="number"> 182 /// <item> 183 /// <description>Any attached <see cref="LocalisableDescriptionAttribute"/>.</description> 184 /// </item> 185 /// <item> 186 /// <description><see cref="GetDescription"/></description> 187 /// </item> 188 /// </list> 189 /// </summary> 190 /// <exception cref="InvalidOperationException"> 191 /// When the <see cref="LocalisableDescriptionAttribute.Name"/> specified in the <see cref="LocalisableDescriptionAttribute"/> 192 /// does not match any of the existing members in <see cref="LocalisableDescriptionAttribute.DeclaringType"/>. 193 /// </exception> 194 public static LocalisableString GetLocalisableDescription<T>(this T value) 195 { 196 MemberInfo type; 197 198 if (value is Enum) 199 type = value.GetType().GetField(value.ToString()); 200 else 201 type = value.GetType(); 202 203 var attribute = type.GetCustomAttribute<LocalisableDescriptionAttribute>(); 204 if (attribute == null) 205 return GetDescription(value); 206 207 var property = attribute.DeclaringType.GetMember(attribute.Name, BindingFlags.Static | BindingFlags.Public).FirstOrDefault(); 208 209 switch (property) 210 { 211 case FieldInfo f: 212 return (LocalisableString)f.GetValue(null).AsNonNull(); 213 214 case PropertyInfo p: 215 return (LocalisableString)p.GetValue(null).AsNonNull(); 216 217 default: 218 throw new InvalidOperationException($"Member \"{attribute.Name}\" was not found in type {attribute.DeclaringType} (must be a static field or property)"); 219 } 220 } 221 222 /// <summary> 223 /// Returns the description of a given object, via (in order): 224 /// <list type="number"> 225 /// <item> 226 /// <description>Any attached <see cref="DescriptionAttribute"/>.</description> 227 /// </item> 228 /// <item> 229 /// <description>The object's <see cref="object.ToString()"/>.</description> 230 /// </item> 231 /// </list> 232 /// </summary> 233 public static string GetDescription(this object value) 234 => value.GetType() 235 .GetField(value.ToString())? 236 .GetCustomAttribute<DescriptionAttribute>()?.Description 237 ?? value.ToString(); 238 239 private static string toLowercaseHex(this byte[] bytes) 240 { 241 // Convert.ToHexString is upper-case, so we are doing this ourselves 242 243 return string.Create(bytes.Length * 2, bytes, (span, b) => 244 { 245 for (int i = 0; i < b.Length; i++) 246 _ = b[i].TryFormat(span[(i * 2)..], out _, "x2"); 247 }); 248 } 249 250 /// <summary> 251 /// Gets a SHA-2 (256bit) hash for the given stream, seeking the stream before and after. 252 /// </summary> 253 /// <param name="stream">The stream to create a hash from.</param> 254 /// <returns>A lower-case hex string representation of the hash (64 characters).</returns> 255 public static string ComputeSHA2Hash(this Stream stream) 256 { 257 string hash; 258 259 stream.Seek(0, SeekOrigin.Begin); 260 261 using (var alg = SHA256.Create()) 262 hash = alg.ComputeHash(stream).toLowercaseHex(); 263 264 stream.Seek(0, SeekOrigin.Begin); 265 266 return hash; 267 } 268 269 /// <summary> 270 /// Gets a SHA-2 (256bit) hash for the given string. 271 /// </summary> 272 /// <param name="str">The string to create a hash from.</param> 273 /// <returns>A lower-case hex string representation of the hash (64 characters).</returns> 274 public static string ComputeSHA2Hash(this string str) 275 { 276 using (var alg = SHA256.Create()) 277 return alg.ComputeHash(Encoding.UTF8.GetBytes(str)).toLowercaseHex(); 278 } 279 280 public static string ComputeMD5Hash(this Stream stream) 281 { 282 string hash; 283 284 stream.Seek(0, SeekOrigin.Begin); 285 using (var md5 = MD5.Create()) 286 hash = md5.ComputeHash(stream).toLowercaseHex(); 287 stream.Seek(0, SeekOrigin.Begin); 288 289 return hash; 290 } 291 292 public static string ComputeMD5Hash(this string input) 293 { 294 using (var md5 = MD5.Create()) 295 return md5.ComputeHash(Encoding.UTF8.GetBytes(input)).toLowercaseHex(); 296 } 297 298 public static DisplayIndex GetIndex(this DisplayDevice display) 299 { 300 if (display == null) return DisplayIndex.Default; 301 302 for (int i = 0; true; i++) 303 { 304 var device = DisplayDevice.GetDisplay((DisplayIndex)i); 305 if (device == null) return DisplayIndex.Default; 306 if (device == display) return (DisplayIndex)i; 307 } 308 } 309 310 /// <summary> 311 /// Standardise the path string using '/' as directory separator. 312 /// Useful as output. 313 /// </summary> 314 /// <param name="path">The path string to standardise.</param> 315 /// <returns>The standardised path string.</returns> 316 public static string ToStandardisedPath(this string path) 317 => path.Replace('\\', '/'); 318 319 /// <summary> 320 /// Converts an osuTK <see cref="DisplayDevice"/> to a <see cref="Display"/> structure. 321 /// </summary> 322 /// <param name="device">The <see cref="DisplayDevice"/> to convert.</param> 323 /// <returns>A <see cref="Display"/> structure populated with the corresponding properties and <see cref="DisplayMode"/>s.</returns> 324 internal static Display ToDisplay(this DisplayDevice device) => 325 new Display((int)device.GetIndex(), device.GetIndex().ToString(), device.Bounds, device.AvailableResolutions.Select(ToDisplayMode).ToArray()); 326 327 /// <summary> 328 /// Converts an osuTK <see cref="DisplayResolution"/> to a <see cref="DisplayMode"/> structure. 329 /// It is not possible to retrieve the pixel format from <see cref="DisplayResolution"/>. 330 /// </summary> 331 /// <param name="resolution">The <see cref="DisplayResolution"/> to convert.</param> 332 /// <returns>A <see cref="DisplayMode"/> structure populated with the corresponding properties.</returns> 333 internal static DisplayMode ToDisplayMode(this DisplayResolution resolution) => 334 new DisplayMode(null, new Size(resolution.Width, resolution.Height), resolution.BitsPerPixel, (int)Math.Round(resolution.RefreshRate), 0, 0); 335 } 336}