/// <summary> /// Converts a Malody object to Qua /// </summary> /// <returns>Qua object</returns> public Qua ToQua() { var audioFile = Hitobjects.FirstOrDefault(x => x.Type == 1).Sound; var audioOffset = -Hitobjects.FirstOrDefault(x => x.Type == 1).Offset; var qua = new Qua() { AudioFile = audioFile, SongPreviewTime = Meta.PreviewTime, BackgroundFile = Meta.Background, MapId = -1, MapSetId = -1, Title = string.IsNullOrEmpty(Meta.Song.Title) ? Meta.Song.TitleOriginal : Meta.Song.Title, Artist = string.IsNullOrEmpty(Meta.Song.Artist) ? Meta.Song.ArtistOriginal : Meta.Song.Artist, Source = "Malody", Tags = "", Creator = Meta.Creator, DifficultyName = Meta.Version, Description = $"This is a Quaver converted version of {Meta.Creator}'s map." }; if (Meta.Mode != 0) throw new ArgumentException("Only the 'Key' Malody game mode can be converted to Qua"); switch (Meta.Keymode.Keymode) { case 4: qua.Mode = GameMode.Keys4; break; case 7: qua.Mode = GameMode.Keys7; break; default: throw new InvalidEnumArgumentException(); } foreach (var tp in TimingPoints) { qua.TimingPoints.Add(new TimingPointInfo() { StartTime = GetMilliSeconds(GetBeat(tp.Beat), audioOffset), Bpm = tp.Bpm, Signature = TimeSignature.Quadruple }); } if (SvPoints != null) { foreach (var sv in SvPoints) { qua.SliderVelocities.Add(new SliderVelocityInfo { StartTime = GetMilliSeconds(GetBeat(sv.Beat), audioOffset), Multiplier = sv.Scroll }); } } foreach (var ho in Hitobjects) { KeySoundInfo keySound; if (ho.Type == 1) // The song itself, doesn't have a note representation continue; if (string.IsNullOrEmpty(ho.Sound)) keySound = null; else { var cas = new CustomAudioSampleInfo { Path = ho.Sound, UnaffectedByRate = false }; if (!qua.CustomAudioSamples.Contains(cas)) qua.CustomAudioSamples.Add(cas); keySound = new KeySoundInfo { Sample = qua.CustomAudioSamples.IndexOf(cas), Volume = ho.Volume }; } qua.HitObjects.Add(new HitObjectInfo { StartTime = GetMilliSeconds(GetBeat(ho.Beat), audioOffset), EndTime = ho.BeatEnd == null ? 0 : GetMilliSeconds(GetBeat(ho.BeatEnd), audioOffset), Lane = ho.Column + 1, KeySounds = keySound == null ? new List<KeySoundInfo>() : new List<KeySoundInfo> { keySound } }); } qua.Sort(); if (!qua.IsValid()) throw new ArgumentException("The .qua file is invalid. It does not have HitObjects, TimingPoints, its Mode is invalid or some hit objects are invalid."); return qua; }
/// <summary> /// Converts an .osu file into a Qua object /// </summary> /// <returns></returns> public Qua ToQua() { // Init Qua with general information var qua = new Qua() { AudioFile = AudioFilename, SongPreviewTime = PreviewTime, BackgroundFile = Background, MapId = -1, MapSetId = -1, Title = Title, Artist = Artist, Source = Source, Tags = Tags, Creator = Creator, DifficultyName = Version, Description = $"This is a Quaver converted version of {Creator}'s map." }; // Get the correct game mode based on the amount of keys the map has. switch (KeyCount) { case 4: qua.Mode = GameMode.Keys4; break; case 7: qua.Mode = GameMode.Keys7; break; case 8: qua.Mode = GameMode.Keys7; qua.HasScratchKey = true; break; default: qua.Mode = (GameMode)(-1); break; } foreach (var path in CustomAudioSamples) { qua.CustomAudioSamples.Add(new CustomAudioSampleInfo() { Path = path, UnaffectedByRate = false }); } // Get custom audio samples and sound effects. foreach (var info in SoundEffects) { // Skip sound effects with zero volume. In Qua 0 is the default value which in this case equals to 100. if (info.Volume == 0) { continue; } qua.SoundEffects.Add(new SoundEffectInfo() { StartTime = info.StartTime, Sample = info.Sample + 1, Volume = info.Volume }); } // Get timing points and slider velocities. foreach (var tp in TimingPoints) { // WARNING: As far as I can tell, BPM changes with BPM < 0 behave as SVs for all intents and purposes, // except that they also participate in the common BPM computation (as negative values). // However, I don't think there are any maps that have enough negative BPM for the common BPM // to become negative, and I am also not sure that negative common BPM wouldn't just break // everything in osu! itself, so instead of inserting a ton of hacks throughout the rest of // Quaver's code base, I'm making negative BPM changes behave fully like SVs (so they are never // taken into account in common BPM computation). If there happens to be an actually working // map with negative common BPM, this is the place to revisit. // -- YaLTeR var isSV = tp.Inherited == 0 || tp.MillisecondsPerBeat < 0; if (isSV) { qua.SliderVelocities.Add(new SliderVelocityInfo { StartTime = tp.Offset, Multiplier = (-100 / tp.MillisecondsPerBeat).Clamp(0.1f, 10) }); } else { qua.TimingPoints.Add(new TimingPointInfo { StartTime = tp.Offset, Bpm = 60000 / tp.MillisecondsPerBeat, Signature = tp.Signature }); } } // Get HitObject Info foreach (var hitObject in HitObjects) { // Get the keyLane the hitObject is in var keyLane = (int)(hitObject.X / (512d / KeyCount)).Clamp(0, KeyCount - 1) + 1; // osu! considers objects in lane 1 to be the special key, Quaver considers it to be the last lane. // Lane 8 on 7K+1 if (qua.HasScratchKey) { if (keyLane == 1) { keyLane = KeyCount; } else { keyLane--; } } // Add HitObjects to the list depending on the object type if (hitObject.Type.HasFlag(HitObjectType.Circle)) { qua.HitObjects.Add(new HitObjectInfo { StartTime = hitObject.StartTime, Lane = keyLane, EndTime = 0, HitSound = hitObject.HitSound.ToQuaverHitSounds(), KeySounds = hitObject.KeySound == -1 ? new List <KeySoundInfo>() : new List <KeySoundInfo> { new KeySoundInfo { Sample = hitObject.KeySound + 1, Volume = hitObject.Volume } } }); } else if (hitObject.Type.HasFlag(HitObjectType.Hold)) { qua.HitObjects.Add(new HitObjectInfo { StartTime = hitObject.StartTime, Lane = keyLane, EndTime = hitObject.EndTime, HitSound = hitObject.HitSound.ToQuaverHitSounds(), KeySounds = hitObject.KeySound == -1 ? new List <KeySoundInfo>() : new List <KeySoundInfo> { new KeySoundInfo { Sample = hitObject.KeySound + 1, Volume = hitObject.Volume } } }); } } // Sort the various lists. qua.Sort(); if (TimingPoints.Count > 0) { // If the individual object key sound volume is zero, we need to set it to the timing point sample volume. var timingPointIndex = 0; var volume = TimingPoints[timingPointIndex].Volume; // Assumption: hit objects and timing points are sorted by start time (enforced by qua.Sort() above). foreach (var hitObject in qua.HitObjects) { // Advance the current timing point index as necessary. while (timingPointIndex < TimingPoints.Count - 1 && TimingPoints[timingPointIndex + 1].Offset <= hitObject.StartTime) { timingPointIndex++; volume = TimingPoints[timingPointIndex].Volume; } for (var i = hitObject.KeySounds.Count - 1; i >= 0; i--) { var keySound = hitObject.KeySounds[i]; if (keySound.Volume == 0) { keySound.Volume = volume; } // If the volume is still zero, remove this key sound. // In Qua 0 is the default value which equals to 100. if (keySound.Volume == 0) { hitObject.KeySounds.RemoveAt(i); } } } } // Do a validity check. if (!qua.IsValid()) { throw new ArgumentException("The .qua file is invalid. It does not have HitObjects, TimingPoints, its Mode is invalid or some hit objects are invalid."); } return(qua); }