1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 21:02:55 +08:00
osu-lazer/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs

461 lines
17 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2018-11-20 15:51:59 +08:00
using osuTK;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
2018-07-02 13:20:35 +08:00
using System.IO;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
using JetBrains.Annotations;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils;
2019-12-10 19:19:16 +08:00
using osu.Game.Beatmaps.Legacy;
using osu.Game.Skinning;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Objects.Legacy
{
/// <summary>
/// A HitObjectParser to parse legacy Beatmaps.
/// </summary>
public abstract class ConvertHitObjectParser : HitObjectParser
{
/// <summary>
/// The offset to apply to all time values.
/// </summary>
2018-08-15 10:47:31 +08:00
protected readonly double Offset;
/// <summary>
/// The beatmap version.
/// </summary>
2018-08-15 10:47:31 +08:00
protected readonly int FormatVersion;
protected bool FirstObject { get; private set; } = true;
protected ConvertHitObjectParser(double offset, int formatVersion)
{
Offset = offset;
2018-08-15 09:53:25 +08:00
FormatVersion = formatVersion;
}
[CanBeNull]
public override HitObject Parse(string text)
2018-04-13 17:19:50 +08:00
{
string[] split = text.Split(',');
2018-04-13 17:19:50 +08:00
Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE));
double startTime = Parsing.ParseDouble(split[2]) + Offset;
2019-12-10 19:19:16 +08:00
LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]);
2019-12-10 19:19:16 +08:00
int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4;
type &= ~LegacyHitObjectType.ComboOffset;
2019-12-10 19:19:16 +08:00
bool combo = type.HasFlag(LegacyHitObjectType.NewCombo);
type &= ~LegacyHitObjectType.NewCombo;
2018-04-13 17:19:50 +08:00
2019-12-10 19:04:37 +08:00
var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
var bankInfo = new SampleBankInfo();
2018-04-13 17:19:50 +08:00
HitObject result = null;
2018-04-13 17:19:50 +08:00
2019-12-10 19:19:16 +08:00
if (type.HasFlag(LegacyHitObjectType.Circle))
{
result = CreateHit(pos, combo, comboOffset);
2018-04-13 17:19:50 +08:00
if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo);
}
2019-12-10 19:19:16 +08:00
else if (type.HasFlag(LegacyHitObjectType.Slider))
{
PathType pathType = PathType.Catmull;
double? length = null;
2018-04-13 17:19:50 +08:00
string[] pointSplit = split[5].Split('|');
2018-10-11 16:44:25 +08:00
int pointCount = 1;
2019-11-11 20:05:36 +08:00
foreach (var t in pointSplit)
2019-11-11 19:53:22 +08:00
{
if (t.Length > 1)
pointCount++;
2019-11-11 19:53:22 +08:00
}
2018-10-11 16:44:25 +08:00
var points = new Vector2[pointCount];
2018-10-11 16:44:25 +08:00
int pointIndex = 1;
2019-04-01 11:16:05 +08:00
foreach (string t in pointSplit)
{
if (t.Length == 1)
2018-04-13 17:19:50 +08:00
{
switch (t)
2018-04-13 17:19:50 +08:00
{
case @"C":
pathType = PathType.Catmull;
break;
case @"B":
pathType = PathType.Bezier;
break;
case @"L":
pathType = PathType.Linear;
break;
case @"P":
pathType = PathType.PerfectCurve;
break;
2018-04-13 17:19:50 +08:00
}
continue;
2018-04-13 17:19:50 +08:00
}
string[] temp = t.Split(':');
points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
}
2018-04-13 17:19:50 +08:00
int repeatCount = Parsing.ParseInt(split[6]);
2018-04-13 17:19:50 +08:00
if (repeatCount > 9000)
2019-11-28 22:21:21 +08:00
throw new FormatException(@"Repeat count is way too high");
2018-04-13 17:19:50 +08:00
// osu-stable treated the first span of the slider as a repeat, but no repeats are happening
repeatCount = Math.Max(0, repeatCount - 1);
2018-04-13 17:19:50 +08:00
if (split.Length > 7)
{
length = Math.Max(0, Parsing.ParseDouble(split[7], Parsing.MAX_COORDINATE_VALUE));
if (length == 0)
length = null;
}
2018-04-13 17:19:50 +08:00
if (split.Length > 10)
readCustomSampleBanks(split[10], bankInfo);
2018-04-13 17:19:50 +08:00
// One node for each repeat + the start and end nodes
int nodes = repeatCount + 2;
2019-04-01 11:16:05 +08:00
// Populate node sample bank infos with the default hit object sample bank
var nodeBankInfos = new List<SampleBankInfo>();
for (int i = 0; i < nodes; i++)
nodeBankInfos.Add(bankInfo.Clone());
2018-04-13 17:19:50 +08:00
// Read any per-node sample banks
if (split.Length > 9 && split[9].Length > 0)
{
string[] sets = split[9].Split('|');
2018-04-13 17:19:50 +08:00
for (int i = 0; i < nodes; i++)
{
if (i >= sets.Length)
break;
2019-04-01 11:16:05 +08:00
SampleBankInfo info = nodeBankInfos[i];
readCustomSampleBanks(sets[i], info);
2018-04-13 17:19:50 +08:00
}
}
// Populate node sound types with the default hit object sound type
2019-12-10 19:04:37 +08:00
var nodeSoundTypes = new List<LegacyHitSoundType>();
for (int i = 0; i < nodes; i++)
nodeSoundTypes.Add(soundType);
2018-04-13 17:19:50 +08:00
// Read any per-node sound types
if (split.Length > 8 && split[8].Length > 0)
2018-04-13 17:19:50 +08:00
{
string[] adds = split[8].Split('|');
2018-04-13 17:19:50 +08:00
for (int i = 0; i < nodes; i++)
2018-04-13 17:19:50 +08:00
{
if (i >= adds.Length)
break;
2018-04-13 17:19:50 +08:00
2019-11-12 18:22:35 +08:00
int.TryParse(adds[i], out var sound);
2019-12-10 19:04:37 +08:00
nodeSoundTypes[i] = (LegacyHitSoundType)sound;
}
}
2018-04-13 17:19:50 +08:00
// Generate the final per-node samples
2019-11-08 13:04:57 +08:00
var nodeSamples = new List<IList<HitSampleInfo>>(nodes);
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
2018-04-13 17:19:50 +08:00
// The samples are played when the slider ends, which is the last node
2019-12-14 20:54:22 +08:00
result.Samples = nodeSamples[^1];
2018-04-13 17:19:50 +08:00
}
2019-12-10 19:19:16 +08:00
else if (type.HasFlag(LegacyHitObjectType.Spinner))
2018-04-13 17:19:50 +08:00
{
2020-05-27 11:37:44 +08:00
double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime);
2020-05-27 11:37:44 +08:00
result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration);
if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo);
2018-04-13 17:19:50 +08:00
}
2019-12-10 19:19:16 +08:00
else if (type.HasFlag(LegacyHitObjectType.Hold))
2019-03-13 10:30:33 +08:00
{
// Note: Hold is generated by BMS converts
double endTime = Math.Max(startTime, Parsing.ParseDouble(split[2]));
if (split.Length > 5 && !string.IsNullOrEmpty(split[5]))
{
string[] ss = split[5].Split(':');
endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0]));
readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
}
2020-05-27 11:37:44 +08:00
result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime);
2019-03-13 10:30:33 +08:00
}
if (result == null)
2019-08-08 13:44:49 +08:00
throw new InvalidDataException($"Unknown hit object type: {split[3]}");
result.StartTime = startTime;
if (result.Samples.Count == 0)
result.Samples = convertSoundType(soundType, bankInfo);
FirstObject = false;
return result;
2018-04-13 17:19:50 +08:00
}
private void readCustomSampleBanks(string str, SampleBankInfo bankInfo)
{
if (string.IsNullOrEmpty(str))
return;
string[] split = str.Split(':');
2019-12-10 19:23:15 +08:00
var bank = (LegacySampleBank)Parsing.ParseInt(split[0]);
var addbank = (LegacySampleBank)Parsing.ParseInt(split[1]);
2018-04-13 17:19:50 +08:00
2018-07-25 13:37:05 +08:00
string stringBank = bank.ToString().ToLowerInvariant();
2018-04-13 17:19:50 +08:00
if (stringBank == @"none")
stringBank = null;
2018-07-25 13:37:05 +08:00
string stringAddBank = addbank.ToString().ToLowerInvariant();
2018-04-13 17:19:50 +08:00
if (stringAddBank == @"none")
stringAddBank = null;
bankInfo.Normal = stringBank;
bankInfo.Add = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
2018-04-13 17:19:50 +08:00
if (split.Length > 2)
bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);
2018-04-13 17:19:50 +08:00
if (split.Length > 3)
bankInfo.Volume = Math.Max(0, Parsing.ParseInt(split[3]));
2018-07-02 13:29:18 +08:00
bankInfo.Filename = split.Length > 4 ? split[4] : null;
2018-04-13 17:19:50 +08:00
}
private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type)
{
if (type == PathType.PerfectCurve)
{
2019-12-10 12:12:54 +08:00
if (vertices.Length != 3)
type = PathType.Bezier;
else if (isLinear(vertices))
{
// osu-stable special-cased colinear perfect curves to a linear path
2019-12-10 12:12:54 +08:00
type = PathType.Linear;
}
}
var points = new List<PathControlPoint>(vertices.Length)
{
new PathControlPoint
{
Position = { Value = vertices[0] },
Type = { Value = type }
}
};
for (int i = 1; i < vertices.Length; i++)
{
if (vertices[i] == vertices[i - 1])
{
2019-12-14 20:54:22 +08:00
points[^1].Type.Value = type;
continue;
}
points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
}
return points.ToArray();
static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Creates a legacy Hit-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2018-04-13 17:19:50 +08:00
/// <returns>The hit object.</returns>
protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset);
2018-04-13 17:19:50 +08:00
/// <summary>
/// Creats a legacy Slider-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2018-04-13 17:19:50 +08:00
/// <param name="controlPoints">The slider control points.</param>
/// <param name="length">The slider length.</param>
/// <param name="repeatCount">The slider repeat count.</param>
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
2018-04-13 17:19:50 +08:00
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
2019-11-08 13:04:57 +08:00
List<IList<HitSampleInfo>> nodeSamples);
2018-04-13 17:19:50 +08:00
/// <summary>
/// Creates a legacy Spinner-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2020-05-27 11:37:44 +08:00
/// <param name="duration">The spinner duration.</param>
2018-04-13 17:19:50 +08:00
/// <returns>The hit object.</returns>
2020-05-27 11:37:44 +08:00
protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration);
2018-04-13 17:19:50 +08:00
/// <summary>
/// Creates a legacy Hold-type hit object.
/// </summary>
/// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
2020-05-31 21:39:03 +08:00
/// <param name="duration">The hold duration.</param>
2020-05-27 11:37:44 +08:00
protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration);
2018-04-13 17:19:50 +08:00
2019-12-10 19:04:37 +08:00
private List<HitSampleInfo> convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
2018-04-13 17:19:50 +08:00
{
2018-07-02 13:20:35 +08:00
// Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
if (!string.IsNullOrEmpty(bankInfo.Filename))
{
2019-06-30 20:58:30 +08:00
return new List<HitSampleInfo>
{
2019-06-30 20:58:30 +08:00
new FileHitSampleInfo
{
Filename = bankInfo.Filename,
Volume = bankInfo.Volume
}
};
}
2018-07-02 13:20:35 +08:00
2019-06-30 20:58:30 +08:00
var soundTypes = new List<HitSampleInfo>
2018-04-13 17:19:50 +08:00
{
2019-06-30 20:58:30 +08:00
new LegacyHitSampleInfo
2018-04-13 17:19:50 +08:00
{
Bank = bankInfo.Normal,
2019-06-30 20:58:30 +08:00
Name = HitSampleInfo.HIT_NORMAL,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)
2018-04-13 17:19:50 +08:00
}
};
2019-12-10 19:04:37 +08:00
if (type.HasFlag(LegacyHitSoundType.Finish))
2018-04-13 17:19:50 +08:00
{
2019-06-30 20:58:30 +08:00
soundTypes.Add(new LegacyHitSampleInfo
2018-04-13 17:19:50 +08:00
{
Bank = bankInfo.Add,
2019-06-30 20:58:30 +08:00
Name = HitSampleInfo.HIT_FINISH,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
2018-04-13 17:19:50 +08:00
});
}
2019-12-10 19:04:37 +08:00
if (type.HasFlag(LegacyHitSoundType.Whistle))
2018-04-13 17:19:50 +08:00
{
2019-06-30 20:58:30 +08:00
soundTypes.Add(new LegacyHitSampleInfo
2018-04-13 17:19:50 +08:00
{
Bank = bankInfo.Add,
2019-06-30 20:58:30 +08:00
Name = HitSampleInfo.HIT_WHISTLE,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
2018-04-13 17:19:50 +08:00
});
}
2019-12-10 19:04:37 +08:00
if (type.HasFlag(LegacyHitSoundType.Clap))
2018-04-13 17:19:50 +08:00
{
2019-06-30 20:58:30 +08:00
soundTypes.Add(new LegacyHitSampleInfo
2018-04-13 17:19:50 +08:00
{
Bank = bankInfo.Add,
2019-06-30 20:58:30 +08:00
Name = HitSampleInfo.HIT_CLAP,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
2018-04-13 17:19:50 +08:00
});
}
return soundTypes;
}
private class SampleBankInfo
{
2018-07-02 13:20:35 +08:00
public string Filename;
2018-04-13 17:19:50 +08:00
public string Normal;
public string Add;
public int Volume;
public int CustomSampleBank;
2018-07-02 13:20:35 +08:00
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
public class LegacyHitSampleInfo : HitSampleInfo
{
2020-04-14 20:05:07 +08:00
private int customSampleBank;
public int CustomSampleBank
{
2020-04-14 20:05:07 +08:00
get => customSampleBank;
set
{
2020-04-14 20:05:07 +08:00
customSampleBank = value;
2020-04-14 20:33:32 +08:00
if (value >= 2)
Suffix = value.ToString();
}
}
/// <summary>
/// Whether this hit sample is layered.
/// </summary>
/// <remarks>
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
/// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option.
/// </remarks>
public bool IsLayered { get; set; }
}
2020-04-14 20:05:07 +08:00
private class FileHitSampleInfo : LegacyHitSampleInfo
2018-07-02 13:20:35 +08:00
{
public string Filename;
2020-04-13 19:09:17 +08:00
public FileHitSampleInfo()
{
2020-04-14 20:05:07 +08:00
// Make sure that the LegacyBeatmapSkin does not fall back to the user skin.
// Note that this does not change the lookup names, as they are overridden locally.
CustomSampleBank = 1;
2020-04-13 19:09:17 +08:00
}
2018-07-02 13:20:35 +08:00
public override IEnumerable<string> LookupNames => new[]
2018-04-13 17:19:50 +08:00
{
2018-07-02 13:20:35 +08:00
Filename,
Path.ChangeExtension(Filename, null)
};
2018-04-13 17:19:50 +08:00
}
}
}