2020-03-23 10:07:09 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System ;
2020-03-23 06:49:45 +08:00
using System.Collections.Generic ;
using System.Linq ;
using System.Reflection ;
2018-11-28 12:12:29 +08:00
using Newtonsoft.Json ;
2020-03-23 06:49:45 +08:00
using osu.Framework.Bindables ;
2021-04-16 14:10:53 +08:00
using osu.Framework.Extensions.TypeExtensions ;
2019-03-27 18:29:27 +08:00
using osu.Framework.Graphics.Sprites ;
2020-09-04 19:34:26 +08:00
using osu.Framework.Testing ;
2020-03-23 06:49:45 +08:00
using osu.Game.Configuration ;
2020-03-19 11:43:26 +08:00
using osu.Game.Rulesets.UI ;
2021-04-13 01:10:22 +08:00
using osu.Game.Utils ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// The base class for gameplay modifiers.
/// </summary>
2020-09-04 19:34:26 +08:00
[ExcludeFromDynamicCompile]
2021-08-31 13:38:35 +08:00
public abstract class Mod : IMod , IEquatable < Mod > , IDeepCloneable < Mod >
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// The name of this mod.
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2018-04-13 17:19:50 +08:00
public abstract string Name { get ; }
/// <summary>
/// The shortened name of this mod.
/// </summary>
2018-11-30 16:16:00 +08:00
public abstract string Acronym { get ; }
2018-04-13 17:19:50 +08:00
/// <summary>
/// The icon of this mod.
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2020-01-14 21:22:00 +08:00
public virtual IconUsage ? Icon = > null ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// The type of this mod.
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2018-07-31 17:00:42 +08:00
public virtual ModType Type = > ModType . Fun ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// The user readable description of this mod.
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2021-04-15 13:32:01 +08:00
public abstract string Description { get ; }
2018-04-13 17:19:50 +08:00
2020-03-19 11:43:26 +08:00
/// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
/// </summary>
/// <remarks>
/// Differs from <see cref="Name"/>, as the value of attributes (AR, CS, etc) changeable via the mod
/// are displayed in the tooltip.
/// </remarks>
[JsonIgnore]
2020-03-21 04:34:36 +08:00
public string IconTooltip
{
get
{
2020-03-23 14:20:56 +08:00
string description = SettingDescription ;
return string . IsNullOrEmpty ( description ) ? Name : $"{Name} ({description})" ;
2020-03-21 04:34:36 +08:00
}
}
/// <summary>
/// The description of editable settings of a mod to use in the <see cref="IconTooltip"/>.
/// </summary>
/// <remarks>
/// Parentheses are added to the tooltip, surrounding the value of this property. If this property is <c>string.Empty</c>,
/// the tooltip will not have parentheses.
/// </remarks>
2020-03-23 06:49:45 +08:00
public virtual string SettingDescription
{
get
{
var tooltipTexts = new List < string > ( ) ;
foreach ( ( SettingSourceAttribute attr , PropertyInfo property ) in this . GetOrderedSettingsSourceProperties ( ) )
{
2021-01-03 14:47:15 +08:00
var bindable = ( IBindable ) property . GetValue ( this ) ;
2020-03-23 14:20:56 +08:00
2021-01-03 14:47:15 +08:00
if ( ! bindable . IsDefault )
tooltipTexts . Add ( $"{attr.Label} {bindable}" ) ;
2020-03-23 06:49:45 +08:00
}
2020-03-23 14:20:56 +08:00
return string . Join ( ", " , tooltipTexts . Where ( s = > ! string . IsNullOrEmpty ( s ) ) ) ;
2020-03-23 06:49:45 +08:00
}
}
2020-03-19 11:43:26 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// The score multiplier of this mod.
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2018-04-13 17:19:50 +08:00
public abstract double ScoreMultiplier { get ; }
/// <summary>
/// Returns true if this mod is implemented (and playable).
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2018-04-13 17:19:50 +08:00
public virtual bool HasImplementation = > this is IApplicableMod ;
/// <summary>
2021-06-09 13:17:01 +08:00
/// Whether this mod is playable by an end user.
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from mutliplayer selection, for example).
2018-04-13 17:19:50 +08:00
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2021-06-09 13:17:01 +08:00
public virtual bool UserPlayable = > true ;
2021-07-02 15:55:25 +08:00
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
2021-07-02 11:50:53 +08:00
public virtual bool Ranked = > false ;
2018-04-13 17:19:50 +08:00
2020-01-21 00:06:36 +08:00
/// <summary>
2020-01-21 11:44:22 +08:00
/// Whether this mod requires configuration to apply changes to the game.
2020-01-21 00:06:36 +08:00
/// </summary>
[JsonIgnore]
public virtual bool RequiresConfiguration = > false ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// The mods this mod cannot be enabled with.
/// </summary>
2018-11-28 12:12:29 +08:00
[JsonIgnore]
2019-11-28 21:41:29 +08:00
public virtual Type [ ] IncompatibleMods = > Array . Empty < Type > ( ) ;
2019-03-14 22:39:45 +08:00
2021-08-18 14:17:42 +08:00
private IReadOnlyList < IBindable > settingsBacking ;
/// <summary>
/// A list of the all <see cref="IBindable"/> settings within this mod.
/// </summary>
internal IReadOnlyList < IBindable > Settings = >
settingsBacking ? ? = this . GetSettingsSourceProperties ( )
. Select ( p = > p . Item2 . GetValue ( this ) )
. Cast < IBindable > ( )
. ToList ( ) ;
2019-03-14 22:39:45 +08:00
/// <summary>
/// Creates a copy of this <see cref="Mod"/> initialised to a default state.
/// </summary>
2021-07-19 11:38:22 +08:00
public virtual Mod DeepClone ( )
2020-08-13 18:48:31 +08:00
{
2021-01-01 20:40:40 +08:00
var result = ( Mod ) Activator . CreateInstance ( GetType ( ) ) ;
2021-01-01 08:46:09 +08:00
result . CopyFrom ( this ) ;
return result ;
}
/// <summary>
2021-02-09 12:44:11 +08:00
/// Copies mod setting values from <paramref name="source"/> into this instance, overwriting all existing settings.
2021-01-01 08:46:09 +08:00
/// </summary>
2021-01-10 22:35:53 +08:00
/// <param name="source">The mod to copy properties from.</param>
public void CopyFrom ( Mod source )
2021-01-01 08:46:09 +08:00
{
2021-01-10 22:35:53 +08:00
if ( source . GetType ( ) ! = GetType ( ) )
throw new ArgumentException ( $"Expected mod of type {GetType()}, got {source.GetType()}." , nameof ( source ) ) ;
2020-08-13 18:48:31 +08:00
foreach ( var ( _ , prop ) in this . GetSettingsSourceProperties ( ) )
{
2021-01-09 05:30:12 +08:00
var targetBindable = ( IBindable ) prop . GetValue ( this ) ;
2021-01-10 22:35:53 +08:00
var sourceBindable = ( IBindable ) prop . GetValue ( source ) ;
2020-08-13 18:48:31 +08:00
2021-02-09 12:44:11 +08:00
CopyAdjustedSetting ( targetBindable , sourceBindable ) ;
2020-08-13 18:48:31 +08:00
}
}
2021-01-03 14:48:28 +08:00
/// <summary>
2021-01-03 20:30:21 +08:00
/// When creating copies or clones of a Mod, this method will be called
/// to copy explicitly adjusted user settings from <paramref name="target"/>.
/// The base implementation will transfer the value via <see cref="Bindable{T}.Parse"/>
/// or by binding and unbinding (if <paramref name="source"/> is an <see cref="IBindable"/>)
/// and should be called unless replaced with custom logic.
2021-01-03 14:48:28 +08:00
/// </summary>
2021-01-03 20:30:21 +08:00
/// <param name="target">The target bindable to apply the adjustment to.</param>
2021-01-03 20:25:44 +08:00
/// <param name="source">The adjustment to apply.</param>
internal virtual void CopyAdjustedSetting ( IBindable target , object source )
2021-01-03 19:44:57 +08:00
{
2021-01-03 20:25:44 +08:00
if ( source is IBindable sourceBindable )
2021-01-03 19:44:57 +08:00
{
2021-01-03 20:30:21 +08:00
// copy including transfer of default values.
2021-01-03 20:25:44 +08:00
target . BindTo ( sourceBindable ) ;
target . UnbindFrom ( sourceBindable ) ;
2020-08-13 18:48:31 +08:00
}
2021-01-03 19:44:57 +08:00
else
2021-04-16 14:10:53 +08:00
{
if ( ! ( target is IParseable parseable ) )
2021-04-16 14:40:06 +08:00
throw new InvalidOperationException ( $"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}." ) ;
2021-04-16 14:10:53 +08:00
parseable . Parse ( source ) ;
}
2020-08-13 18:48:31 +08:00
}
2019-05-03 09:05:45 +08:00
2020-12-28 20:19:28 +08:00
public bool Equals ( IMod other ) = > other is Mod them & & Equals ( them ) ;
2021-04-13 01:10:22 +08:00
public bool Equals ( Mod other )
{
if ( ReferenceEquals ( null , other ) ) return false ;
if ( ReferenceEquals ( this , other ) ) return true ;
return GetType ( ) = = other . GetType ( ) & &
2021-08-18 14:17:42 +08:00
Settings . SequenceEqual ( other . Settings , ModSettingsEqualityComparer . Default ) ;
2021-04-13 01:10:22 +08:00
}
2021-02-09 12:44:42 +08:00
2021-08-17 09:27:04 +08:00
public override int GetHashCode ( )
{
var hashCode = new HashCode ( ) ;
hashCode . Add ( GetType ( ) ) ;
2021-08-18 14:17:42 +08:00
foreach ( var setting in Settings )
hashCode . Add ( ModUtils . GetSettingUnderlyingValue ( setting ) ) ;
2021-08-17 09:27:04 +08:00
return hashCode . ToHashCode ( ) ;
}
2021-02-09 12:44:42 +08:00
/// <summary>
/// Reset all custom settings for this mod back to their defaults.
/// </summary>
public virtual void ResetSettingsToDefaults ( ) = > CopyFrom ( ( Mod ) Activator . CreateInstance ( GetType ( ) ) ) ;
2021-08-18 14:17:42 +08:00
private class ModSettingsEqualityComparer : IEqualityComparer < IBindable >
{
public static ModSettingsEqualityComparer Default { get ; } = new ModSettingsEqualityComparer ( ) ;
public bool Equals ( IBindable x , IBindable y )
{
object xValue = x = = null ? null : ModUtils . GetSettingUnderlyingValue ( x ) ;
object yValue = y = = null ? null : ModUtils . GetSettingUnderlyingValue ( y ) ;
return EqualityComparer < object > . Default . Equals ( xValue , yValue ) ;
}
public int GetHashCode ( IBindable obj ) = > ModUtils . GetSettingUnderlyingValue ( obj ) . GetHashCode ( ) ;
}
2018-04-13 17:19:50 +08:00
}
}