A game framework written with osu! in mind.
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}