// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using AutoMapper.Internal; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Rulesets.UI; using osu.Game.Utils; namespace osu.Game.Rulesets.Mods { /// /// The base class for gameplay modifiers. /// [ExcludeFromDynamicCompile] public abstract class Mod : IMod, IEquatable, IDeepCloneable { [JsonIgnore] public abstract string Name { get; } public abstract string Acronym { get; } [JsonIgnore] public virtual IconUsage? Icon => null; [JsonIgnore] public virtual ModType Type => ModType.Fun; [JsonIgnore] public abstract LocalisableString Description { get; } /// /// The tooltip to display for this mod when used in a . /// /// /// Differs from , as the value of attributes (AR, CS, etc) changeable via the mod /// are displayed in the tooltip. /// [JsonIgnore] public string IconTooltip { get { string description = SettingDescription; return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})"; } } /// /// The description of editable settings of a mod to use in the . /// /// /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, /// the tooltip will not have parentheses. /// public virtual string SettingDescription { get { var tooltipTexts = new List(); foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { var bindable = (IBindable)property.GetValue(this)!; if (!bindable.IsDefault) tooltipTexts.Add($"{attr.Label} {bindable}"); } return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); } } /// /// The score multiplier of this mod. /// [JsonIgnore] public abstract double ScoreMultiplier { get; } /// /// Returns true if this mod is implemented (and playable). /// [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; [JsonIgnore] public virtual bool UserPlayable => true; [JsonIgnore] public virtual bool ValidForMultiplayer => true; [JsonIgnore] public virtual bool ValidForMultiplayerAsFreeMod => true; /// /// Whether this mod requires configuration to apply changes to the game. /// [JsonIgnore] public virtual bool RequiresConfiguration => false; /// /// The mods this mod cannot be enabled with. /// [JsonIgnore] public virtual Type[] IncompatibleMods => Array.Empty(); private IReadOnlyList? settingsBacking; /// /// A list of the all settings within this mod. /// internal IReadOnlyList Settings => settingsBacking ??= this.GetSettingsSourceProperties() .Select(p => p.Item2.GetValue(this)) .Cast() .ToList(); /// /// Whether all settings in this mod are set to their default state. /// protected virtual bool UsesDefaultConfiguration => Settings.All(s => s.IsDefault); /// /// Creates a copy of this initialised to a default state. /// public virtual Mod DeepClone() { var result = (Mod)Activator.CreateInstance(GetType())!; result.CopyFrom(this); return result; } /// /// Copies mod setting values from into this instance, overwriting all existing settings. /// /// The mod to copy properties from. public void CopyFrom(Mod source) { if (source.GetType() != GetType()) throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); foreach (var (_, property) in this.GetSettingsSourceProperties()) { var targetBindable = (IBindable)property.GetValue(this)!; var sourceBindable = (IBindable)property.GetValue(source)!; CopyAdjustedSetting(targetBindable, sourceBindable); } } /// /// Copies all mod setting values sharing same from into this instance. /// /// The mod to copy properties from. internal void CopySharedSettings(Mod source) { Dictionary oldSettings = new Dictionary(); foreach (var (_, property) in source.GetSettingsSourceProperties()) { oldSettings.Add(property.Name.ToSnakeCase(), property.GetValue(source)!); } foreach (var (_, property) in this.GetSettingsSourceProperties()) { object targetSetting = property.GetValue(this)!; if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) continue; if (((IBindable)sourceSetting).IsDefault) // keep at default value if the source is default continue; Type? targetType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); Type? sourceType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); if (targetType == null || sourceType == null) { if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) // change settings only if the type is the same CopyAdjustedSetting((IBindable)targetSetting, sourceSetting); continue; } double targetMin = getValue(targetSetting, nameof(IBindableNumber.MinValue)); double targetMax = getValue(targetSetting, nameof(IBindableNumber.MaxValue)); double sourceMin = getValue(sourceSetting, nameof(IBindableNumber.MinValue)); double sourceMax = getValue(sourceSetting, nameof(IBindableNumber.MaxValue)); double sourceValue = getValue(sourceSetting, nameof(IBindableNumber.Value)); // convert value to same ratio double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; targetType.GetProperty(nameof(IBindableNumber.Value))!.SetValue(targetSetting, Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); } double getValue(object target, string name) => Convert.ToDouble(target.GetType().GetProperty(name)!.GetValue(target)!); Type? getGenericBaseType(object target, Type genericType) => target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); } /// /// When creating copies or clones of a Mod, this method will be called /// to copy explicitly adjusted user settings from . /// The base implementation will transfer the value via /// or by binding and unbinding (if is an ) /// and should be called unless replaced with custom logic. /// /// The target bindable to apply the adjustment to. /// The adjustment to apply. internal virtual void CopyAdjustedSetting(IBindable target, object source) { if (source is IBindable sourceBindable) { // copy including transfer of default values. target.BindTo(sourceBindable); target.UnbindFrom(sourceBindable); } else { if (!(target is IParseable parseable)) throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}."); parseable.Parse(source); } } public bool Equals(IMod? other) => other is Mod them && Equals(them); public bool Equals(Mod? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return GetType() == other.GetType() && Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); } public override int GetHashCode() { var hashCode = new HashCode(); hashCode.Add(GetType()); foreach (var setting in Settings) hashCode.Add(setting.GetUnderlyingSettingValue()); return hashCode.ToHashCode(); } /// /// Reset all custom settings for this mod back to their defaults. /// public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType())!); private class ModSettingsEqualityComparer : IEqualityComparer { public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer(); public bool Equals(IBindable? x, IBindable? y) { object? xValue = x?.GetUnderlyingSettingValue(); object? yValue = y?.GetUnderlyingSettingValue(); return EqualityComparer.Default.Equals(xValue, yValue); } public int GetHashCode(IBindable obj) => obj.GetUnderlyingSettingValue().GetHashCode(); } } }