1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 12:07:23 +08:00

Merge branch 'master' into taiko_replays

This commit is contained in:
Dean Herbert 2017-03-31 17:10:56 +09:00 committed by GitHub
commit 1110747648
24 changed files with 248 additions and 58 deletions

@ -1 +1 @@
Subproject commit 7ae3cf1a10fa973a49995a71cbaf768702be1cce Subproject commit 415884e7e19f9062a4fac457a7ce19b566fa2ee7

View File

@ -56,7 +56,6 @@ namespace osu.Desktop.VisualTests.Tests
Result = HitResult.Hit, Result = HitResult.Hit,
TaikoResult = hitResult, TaikoResult = hitResult,
TimeOffset = 0, TimeOffset = 0,
ComboAtHit = 1,
SecondHit = RNG.Next(10) == 0 SecondHit = RNG.Next(10) == 0
} }
}); });
@ -69,8 +68,7 @@ namespace osu.Desktop.VisualTests.Tests
Judgement = new TaikoJudgement Judgement = new TaikoJudgement
{ {
Result = HitResult.Miss, Result = HitResult.Miss,
TimeOffset = 0, TimeOffset = 0
ComboAtHit = 0
} }
}); });
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Modes.Catch.Scoring
{ {
} }
protected override void OnNewJugement(CatchJudgement judgement) protected override void OnNewJudgement(CatchJudgement judgement)
{ {
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Modes.Mania.Scoring
{ {
} }
protected override void OnNewJugement(ManiaJudgement judgement) protected override void OnNewJudgement(ManiaJudgement judgement)
{ {
} }
} }

View File

@ -38,7 +38,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
Colour = AccentColour, Colour = AccentColour,
Hit = () => Hit = () =>
{ {
if (Judgement.Result.HasValue) return false; if (Judgement.Result != HitResult.None) return false;
Judgement.PositionOffset = Vector2.Zero; //todo: set to correct value Judgement.PositionOffset = Vector2.Zero; //todo: set to correct value
UpdateJudgement(true); UpdateJudgement(true);

View File

@ -28,7 +28,7 @@ namespace osu.Game.Modes.Osu.Scoring
Accuracy.Value = 1; Accuracy.Value = 1;
} }
protected override void OnNewJugement(OsuJudgement judgement) protected override void OnNewJudgement(OsuJudgement judgement)
{ {
if (judgement != null) if (judgement != null)
{ {

View File

@ -22,7 +22,7 @@ namespace osu.Game.Modes.Taiko.Judgements
/// The result value for the combo portion of the score. /// The result value for the combo portion of the score.
/// </summary> /// </summary>
public int ResultValueForScore => NumericResultForScore(TaikoResult); public int ResultValueForScore => NumericResultForScore(TaikoResult);
/// <summary> /// <summary>
/// The result value for the accuracy portion of the score. /// The result value for the accuracy portion of the score.
/// </summary> /// </summary>
@ -32,7 +32,7 @@ namespace osu.Game.Modes.Taiko.Judgements
/// The maximum result value for the combo portion of the score. /// The maximum result value for the combo portion of the score.
/// </summary> /// </summary>
public int MaxResultValueForScore => NumericResultForScore(MAX_HIT_RESULT); public int MaxResultValueForScore => NumericResultForScore(MAX_HIT_RESULT);
/// <summary> /// <summary>
/// The maximum result value for the accuracy portion of the score. /// The maximum result value for the accuracy portion of the score.
/// </summary> /// </summary>
@ -45,7 +45,7 @@ namespace osu.Game.Modes.Taiko.Judgements
/// <summary> /// <summary>
/// Whether this Judgement has a secondary hit in the case of finishers. /// Whether this Judgement has a secondary hit in the case of finishers.
/// </summary> /// </summary>
public bool SecondHit; public virtual bool SecondHit { get; set; }
/// <summary> /// <summary>
/// Computes the numeric result value for the combo portion of the score. /// Computes the numeric result value for the combo portion of the score.

View File

@ -0,0 +1,25 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Modes.Judgements;
namespace osu.Game.Modes.Taiko.Judgements
{
public class TaikoStrongHitJudgement : TaikoJudgement, IPartialJudgement
{
public bool Changed { get; set; }
public override bool SecondHit
{
get { return base.SecondHit; }
set
{
if (base.SecondHit == value)
return;
base.SecondHit = value;
Changed = true;
}
}
}
}

View File

@ -47,7 +47,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
protected override bool HandleKeyPress(Key key) protected override bool HandleKeyPress(Key key)
{ {
return !Judgement.Result.HasValue && UpdateJudgement(true); return Judgement.Result == HitResult.None && UpdateJudgement(true);
} }
} }
} }

View File

@ -68,7 +68,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
protected override bool HandleKeyPress(Key key) protected override bool HandleKeyPress(Key key)
{ {
if (Judgement.Result.HasValue) if (Judgement.Result != HitResult.None)
return false; return false;
validKeyPressed = HitKeys.Contains(key); validKeyPressed = HitKeys.Contains(key);

View File

@ -5,6 +5,8 @@ using OpenTK.Input;
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Modes.Objects.Drawables;
using osu.Game.Modes.Taiko.Judgements;
namespace osu.Game.Modes.Taiko.Objects.Drawable namespace osu.Game.Modes.Taiko.Objects.Drawable
{ {
@ -25,9 +27,11 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
{ {
} }
protected override TaikoJudgement CreateJudgement() => new TaikoStrongHitJudgement();
protected override void CheckJudgement(bool userTriggered) protected override void CheckJudgement(bool userTriggered)
{ {
if (!Judgement.Result.HasValue) if (Judgement.Result == HitResult.None)
{ {
base.CheckJudgement(userTriggered); base.CheckJudgement(userTriggered);
return; return;
@ -45,7 +49,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
protected override bool HandleKeyPress(Key key) protected override bool HandleKeyPress(Key key)
{ {
// Check if we've handled the first key // Check if we've handled the first key
if (!Judgement.Result.HasValue) if (Judgement.Result == HitResult.None)
{ {
// First key hasn't been handled yet, attempt to handle it // First key hasn't been handled yet, attempt to handle it
bool handled = base.HandleKeyPress(key); bool handled = base.HandleKeyPress(key);

View File

@ -219,7 +219,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
protected override bool HandleKeyPress(Key key) protected override bool HandleKeyPress(Key key)
{ {
if (Judgement.Result.HasValue) if (Judgement.Result != HitResult.None)
return false; return false;
// Don't handle keys before the swell starts // Don't handle keys before the swell starts

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using OpenTK.Graphics; using OpenTK.Graphics;
using System; using System;
using osu.Game.Graphics;
namespace osu.Game.Modes.Taiko.Objects.Drawable.Pieces namespace osu.Game.Modes.Taiko.Objects.Drawable.Pieces
{ {
@ -18,7 +19,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable.Pieces
/// a rounded (_[-Width-]_) figure such that a regular "circle" is the result of a parent with Width = 0. /// a rounded (_[-Width-]_) figure such that a regular "circle" is the result of a parent with Width = 0.
/// </para> /// </para>
/// </summary> /// </summary>
public class CirclePiece : Container public class CirclePiece : Container, IHasAccentColour
{ {
public const float SYMBOL_SIZE = TaikoHitObject.CIRCLE_RADIUS * 2f * 0.45f; public const float SYMBOL_SIZE = TaikoHitObject.CIRCLE_RADIUS * 2f * 0.45f;
public const float SYMBOL_BORDER = 8; public const float SYMBOL_BORDER = 8;

View File

@ -96,9 +96,9 @@ namespace osu.Game.Modes.Taiko.Scoring
/// <summary> /// <summary>
/// The multiple of the original score added to the combo portion of the score /// The multiple of the original score added to the combo portion of the score
/// for correctly hitting an accented hit object with both keys. /// for correctly hitting a strong hit object with both keys.
/// </summary> /// </summary>
private double accentedHitScale; private double strongHitScale;
private double hpIncreaseTick; private double hpIncreaseTick;
private double hpIncreaseGreat; private double hpIncreaseGreat;
@ -128,12 +128,12 @@ namespace osu.Game.Modes.Taiko.Scoring
hpIncreaseGood = hpMultiplierNormal * hp_hit_good; hpIncreaseGood = hpMultiplierNormal * hp_hit_good;
hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max); hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max);
var accentedHits = beatmap.HitObjects.FindAll(o => o is Hit && o.IsStrong); var strongHits = beatmap.HitObjects.FindAll(o => o is Hit && o.IsStrong);
// This is a linear function that awards: // This is a linear function that awards:
// 10 times bonus points for hitting an accented hit object with both keys with 30 accented hit objects in the map // 10 times bonus points for hitting a strong hit object with both keys with 30 strong hit objects in the map
// 3 times bonus points for hitting an accented hit object with both keys with 120 accented hit objects in the map // 3 times bonus points for hitting a strong hit object with both keys with 120 strong hit objects in the map
accentedHitScale = -7d / 90d * MathHelper.Clamp(accentedHits.Count, 30, 120) + 111d / 9d; strongHitScale = -7d / 90d * MathHelper.Clamp(strongHits.Count, 30, 120) + 111d / 9d;
foreach (var obj in beatmap.HitObjects) foreach (var obj in beatmap.HitObjects)
{ {
@ -179,7 +179,7 @@ namespace osu.Game.Modes.Taiko.Scoring
maxComboPortion = comboPortion; maxComboPortion = comboPortion;
} }
protected override void OnNewJugement(TaikoJudgement judgement) protected override void OnNewJudgement(TaikoJudgement judgement)
{ {
bool isTick = judgement is TaikoDrumRollTickJudgement; bool isTick = judgement is TaikoDrumRollTickJudgement;
@ -187,29 +187,12 @@ namespace osu.Game.Modes.Taiko.Scoring
if (!isTick) if (!isTick)
totalHits++; totalHits++;
// Apply combo changes, must be done before the hit score is added
if (!isTick && judgement.Result == HitResult.Hit)
Combo.Value++;
// Apply score changes // Apply score changes
if (judgement.Result == HitResult.Hit) addHitScore(judgement);
{
double baseValue = judgement.ResultValueForScore;
// Add bonus points for hitting an accented hit object with the second key
if (judgement.SecondHit)
baseValue += baseValue * accentedHitScale;
// Add score to portions
if (isTick)
bonusScore += baseValue;
else
{
Combo.Value++;
// A relevance factor that needs to be applied to make higher combos more relevant
// Value is capped at 400 combo
double comboRelevance = Math.Min(Math.Log(400, combo_base), Math.Max(0.5, Math.Log(Combo.Value, combo_base)));
comboPortion += baseValue * comboRelevance;
}
}
// Apply HP changes // Apply HP changes
switch (judgement.Result) switch (judgement.Result)
@ -235,7 +218,43 @@ namespace osu.Game.Modes.Taiko.Scoring
break; break;
} }
// Compute the new score + accuracy calculateScore();
}
protected override void OnJudgementChanged(TaikoJudgement judgement)
{
// Apply score changes
addHitScore(judgement);
calculateScore();
}
private void addHitScore(TaikoJudgement judgement)
{
if (judgement.Result != HitResult.Hit)
return;
double baseValue = judgement.ResultValueForScore;
// Add increased score for hitting a strong hit object with the second key
if (judgement.SecondHit)
baseValue *= strongHitScale;
// Add score to portions
if (judgement is TaikoDrumRollTickJudgement)
bonusScore += baseValue;
else
{
// A relevance factor that needs to be applied to make higher combos more relevant
// Value is capped at 400 combo
double comboRelevance = Math.Min(Math.Log(400, combo_base), Math.Max(0.5, Math.Log(Combo.Value, combo_base)));
comboPortion += baseValue * comboRelevance;
}
}
private void calculateScore()
{
int scoreForAccuracy = 0; int scoreForAccuracy = 0;
int maxScoreForAccuracy = 0; int maxScoreForAccuracy = 0;

View File

@ -50,6 +50,7 @@
<Compile Include="Beatmaps\TaikoBeatmapConverter.cs" /> <Compile Include="Beatmaps\TaikoBeatmapConverter.cs" />
<Compile Include="Beatmaps\TaikoBeatmapProcessor.cs" /> <Compile Include="Beatmaps\TaikoBeatmapProcessor.cs" />
<Compile Include="Judgements\TaikoDrumRollTickJudgement.cs" /> <Compile Include="Judgements\TaikoDrumRollTickJudgement.cs" />
<Compile Include="Judgements\TaikoStrongHitJudgement.cs" />
<Compile Include="Judgements\TaikoJudgement.cs" /> <Compile Include="Judgements\TaikoJudgement.cs" />
<Compile Include="Judgements\TaikoHitResult.cs" /> <Compile Include="Judgements\TaikoHitResult.cs" />
<Compile Include="Objects\CentreHit.cs" /> <Compile Include="Objects\CentreHit.cs" />

View File

@ -0,0 +1,34 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Game.Graphics.Transforms;
namespace osu.Game.Graphics
{
/// <summary>
/// A type of drawable that has an accent colour.
/// The accent colour is used to colorize various objects inside a drawable
/// without colorizing the drawable itself.
/// </summary>
public interface IHasAccentColour : IDrawable
{
Color4 AccentColour { get; set; }
}
public static class AccentedColourExtensions
{
/// <summary>
/// Tweens the accent colour of a drawable to another colour.
/// </summary>
/// <param name="accentedDrawable">The drawable to apply the accent colour to.</param>
/// <param name="newColour">The new accent colour.</param>
/// <param name="duration">The tween duration.</param>
/// <param name="easing">The tween easing.</param>
public static void FadeAccent(this IHasAccentColour accentedDrawable, Color4 newColour, double duration = 0, EasingTypes easing = EasingTypes.None)
{
accentedDrawable.TransformTo(accentedDrawable.AccentColour, newColour, duration, easing, new TransformAccent());
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.MathUtils;
namespace osu.Game.Graphics.Transforms
{
public class TransformAccent : Transform<Color4>
{
/// <summary>
/// Current value of the transformed colour in linear colour space.
/// </summary>
public override Color4 CurrentValue
{
get
{
double time = Time?.Current ?? 0;
if (time < StartTime) return StartValue;
if (time >= EndTime) return EndValue;
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
}
}
public override void Apply(Drawable d)
{
base.Apply(d);
var accented = d as IHasAccentColour;
if (accented != null)
accented.AccentColour = CurrentValue;
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Modes.Objects.Drawables;
using osu.Game.Modes.Scoring;
namespace osu.Game.Modes.Judgements
{
/// <summary>
/// Inidicates that the judgement this is attached to is a partial judgement and the scoring value may change.
/// <para>
/// This judgement will be continually processed by <see cref="DrawableHitObject{TObject, TJudgement}.CheckJudgement(bool)"/>
/// unless the result is a miss and will trigger a full re-process of the <see cref="ScoreProcessor"/> when changed.
/// </para>
/// </summary>
public interface IPartialJudgement
{
/// <summary>
/// Indicates that this partial judgement has changed and requires a full re-process of the <see cref="ScoreProcessor"/>.
/// <para>
/// This is set to false once the judgement has been re-processed.
/// </para>
/// </summary>
bool Changed { get; set; }
}
}

View File

@ -10,7 +10,7 @@ namespace osu.Game.Modes.Judgements
/// <summary> /// <summary>
/// Whether this judgement is the result of a hit or a miss. /// Whether this judgement is the result of a hit or a miss.
/// </summary> /// </summary>
public HitResult? Result; public HitResult Result;
/// <summary> /// <summary>
/// The offset at which this judgement occurred. /// The offset at which this judgement occurred.
@ -20,7 +20,7 @@ namespace osu.Game.Modes.Judgements
/// <summary> /// <summary>
/// The combo after this judgement was processed. /// The combo after this judgement was processed.
/// </summary> /// </summary>
public ulong? ComboAtHit; public int ComboAtHit;
/// <summary> /// <summary>
/// The string representation for the result achieved. /// The string representation for the result achieved.

View File

@ -93,16 +93,26 @@ namespace osu.Game.Modes.Objects.Drawables
/// <returns>Whether a hit was processed.</returns> /// <returns>Whether a hit was processed.</returns>
protected bool UpdateJudgement(bool userTriggered) protected bool UpdateJudgement(bool userTriggered)
{ {
if (Judgement.Result != null) IPartialJudgement partial = Judgement as IPartialJudgement;
// Never re-process non-partial hits
if (Judgement.Result != HitResult.None && partial == null)
return false; return false;
// Update the judgement state
double endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; double endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
Judgement.TimeOffset = Time.Current - endTime; Judgement.TimeOffset = Time.Current - endTime;
// Update the judgement state
bool hadResult = Judgement.Result != HitResult.None;
CheckJudgement(userTriggered); CheckJudgement(userTriggered);
if (Judgement.Result == null) // Don't process judgements with no result
if (Judgement.Result == HitResult.None)
return false;
// Don't process judgements that previously had results but the results were unchanged
if (hadResult && partial?.Changed != true)
return false; return false;
switch (Judgement.Result) switch (Judgement.Result)
@ -117,6 +127,9 @@ namespace osu.Game.Modes.Objects.Drawables
OnJudgement?.Invoke(this); OnJudgement?.Invoke(this);
if (partial != null)
partial.Changed = false;
return true; return true;
} }

View File

@ -7,8 +7,19 @@ namespace osu.Game.Modes.Objects.Drawables
{ {
public enum HitResult public enum HitResult
{ {
/// <summary>
/// Indicates that the object has not been judged yet.
/// </summary>
[Description("")]
None,
/// <summary>
/// Indicates that the object has been judged as a miss.
/// </summary>
[Description(@"Miss")] [Description(@"Miss")]
Miss, Miss,
/// <summary>
/// Indicates that the object has been judged as a hit.
/// </summary>
[Description(@"Hit")] [Description(@"Hit")]
Hit, Hit,
} }

View File

@ -141,11 +141,17 @@ namespace osu.Game.Modes.Scoring
/// <param name="judgement">The judgement to add.</param> /// <param name="judgement">The judgement to add.</param>
protected void AddJudgement(TJudgement judgement) protected void AddJudgement(TJudgement judgement)
{ {
Judgements.Add(judgement); bool exists = Judgements.Contains(judgement);
OnNewJugement(judgement); if (!exists)
{
Judgements.Add(judgement);
OnNewJudgement(judgement);
judgement.ComboAtHit = (ulong)Combo.Value; judgement.ComboAtHit = Combo.Value;
}
else
OnJudgementChanged(judgement);
UpdateFailed(); UpdateFailed();
} }
@ -158,9 +164,21 @@ namespace osu.Game.Modes.Scoring
} }
/// <summary> /// <summary>
/// Update any values that potentially need post-processing on a judgement change. /// Updates any values that need post-processing. Invoked when a new judgement has occurred.
/// <para>
/// This is not triggered when existing judgements are changed - for that see <see cref="OnJudgementChanged(TJudgement)"/>.
/// </para>
/// </summary> /// </summary>
/// <param name="judgement">The judgement that triggered this calculation.</param> /// <param name="judgement">The judgement that triggered this calculation.</param>
protected abstract void OnNewJugement(TJudgement judgement); protected abstract void OnNewJudgement(TJudgement judgement);
/// <summary>
/// Updates any values that need post-processing. Invoked when an existing judgement has changed.
/// <para>
/// This is not triggered when a new judgement has occurred - for that see <see cref="OnNewJudgement(TJudgement)"/>.
/// </para>
/// </summary>
/// <param name="judgement">The judgement that triggered this calculation.</param>
protected virtual void OnJudgementChanged(TJudgement judgement) { }
} }
} }

View File

@ -158,7 +158,7 @@ namespace osu.Game.Modes.UI
public event Action<TJudgement> OnJudgement; public event Action<TJudgement> OnJudgement;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
protected override bool AllObjectsJudged => Playfield.HitObjects.Children.All(h => h.Judgement.Result.HasValue); protected override bool AllObjectsJudged => Playfield.HitObjects.Children.All(h => h.Judgement.Result != HitResult.None);
/// <summary> /// <summary>
/// The playfield. /// The playfield.

View File

@ -82,7 +82,9 @@
<Compile Include="Graphics\Backgrounds\Triangles.cs" /> <Compile Include="Graphics\Backgrounds\Triangles.cs" />
<Compile Include="Graphics\Cursor\CursorTrail.cs" /> <Compile Include="Graphics\Cursor\CursorTrail.cs" />
<Compile Include="Graphics\Cursor\GameplayCursor.cs" /> <Compile Include="Graphics\Cursor\GameplayCursor.cs" />
<Compile Include="Graphics\IHasAccentColour.cs" />
<Compile Include="Graphics\Sprites\OsuSpriteText.cs" /> <Compile Include="Graphics\Sprites\OsuSpriteText.cs" />
<Compile Include="Graphics\Transforms\TransformAccent.cs" />
<Compile Include="Graphics\UserInterface\BackButton.cs" /> <Compile Include="Graphics\UserInterface\BackButton.cs" />
<Compile Include="Graphics\UserInterface\FocusedTextBox.cs" /> <Compile Include="Graphics\UserInterface\FocusedTextBox.cs" />
<Compile Include="Graphics\UserInterface\Nub.cs" /> <Compile Include="Graphics\UserInterface\Nub.cs" />
@ -98,6 +100,7 @@
<Compile Include="IPC\ScoreIPCChannel.cs" /> <Compile Include="IPC\ScoreIPCChannel.cs" />
<Compile Include="Modes\Replays\Replay.cs" /> <Compile Include="Modes\Replays\Replay.cs" />
<Compile Include="Modes\Judgements\DrawableJudgement.cs" /> <Compile Include="Modes\Judgements\DrawableJudgement.cs" />
<Compile Include="Modes\Judgements\IPartialJudgement.cs" />
<Compile Include="Modes\Replays\FramedReplayInputHandler.cs" /> <Compile Include="Modes\Replays\FramedReplayInputHandler.cs" />
<Compile Include="Modes\Mods\IApplicableMod.cs" /> <Compile Include="Modes\Mods\IApplicableMod.cs" />
<Compile Include="Modes\Mods\ModType.cs" /> <Compile Include="Modes\Mods\ModType.cs" />