Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement guitar note shuffle #193

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
46 changes: 42 additions & 4 deletions YARG.Core/Chart/Notes/GuitarNote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,46 @@ public class GuitarNote : Note<GuitarNote>
private GuitarNoteFlags _guitarFlags;
public GuitarNoteFlags GuitarFlags;

public int Fret { get; }
public int DisjointMask { get; }
private int _fret;

public int Fret
{
get => _fret;
set
{
if (value == _fret)
return;

int mask = GetNoteMask(value);
int oldMask = GetNoteMask(_fret);

// If we're a child note, adjust our parent's mask to reflect the change
if (Parent != null)
{
if ((Parent.NoteMask & mask) != 0)
throw new InvalidOperationException($"Fret {value} already exists in the current chord!");

Parent.NoteMask &= ~oldMask;
Parent.NoteMask |= mask;

NoteMask = mask;
}
// Otherwise, adjust our own mask
else
{
if ((NoteMask & mask) != 0)
throw new InvalidOperationException($"Fret {value} already exists in the current chord!");

NoteMask &= ~oldMask;
NoteMask |= mask;
}

_fret = value;
DisjointMask = mask;
}
}

public int DisjointMask { get; private set; }
public int NoteMask { get; private set; }

public uint SustainTicksHeld;
Expand Down Expand Up @@ -40,7 +78,7 @@ public GuitarNote(int fret, GuitarNoteType noteType, GuitarNoteFlags guitarFlags
double time, double timeLength, uint tick, uint tickLength)
: base(flags, time, timeLength, tick, tickLength)
{
Fret = fret;
_fret = fret;
Type = noteType;

GuitarFlags = _guitarFlags = guitarFlags;
Expand All @@ -51,7 +89,7 @@ public GuitarNote(int fret, GuitarNoteType noteType, GuitarNoteFlags guitarFlags

public GuitarNote(GuitarNote other) : base(other)
{
Fret = other.Fret;
_fret = other._fret;
Type = other.Type;

GuitarFlags = _guitarFlags = other._guitarFlags;
Expand Down
9 changes: 8 additions & 1 deletion YARG.Core/Chart/Notes/Note.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ public virtual void AddChildNote(TNote note)
_childNotes.Add(note);
}

public virtual void RemoveChildNote(TNote note)
{
if (!_childNotes.Remove(note))
throw new InvalidOperationException("Child note being removed is not part of this note!");
note.Parent = null;
}

public IEnumerable<TNote> ChordEnumerator()
{
yield return (TNote) this;
Expand Down Expand Up @@ -171,7 +178,7 @@ public virtual void ResetNoteState()
}
}

protected static int GetNoteMask(int note)
public static int GetNoteMask(int note)
{
// Resulting shift is 1 too high, shifting down by 1 corrects this.
// Reason for not doing (note - 1) is this breaks open notes. (1 << (0 - 1) == 0x80000000)
Expand Down
6 changes: 6 additions & 0 deletions YARG.Core/Chart/Notes/VocalNote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ public override void AddChildNote(VocalNote note)
TotalTickLength = _childNotes[^1].TickEnd - Tick;
}

public override void RemoveChildNote(VocalNote note)
{
// TODO
throw new NotImplementedException();
}

Comment on lines +185 to +190
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure what to do here and didn't feel like going through the AddChildNote override at the moment, so left it as a TODO.

/// <summary>
/// Adds a child note to this vocal phrase.
/// Use <see cref="AddChildNote"/> instead if this is a note!
Expand Down
96 changes: 96 additions & 0 deletions YARG.Core/Chart/Tracks/InstrumentDifficultyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using YARG.Core.Extensions;
using YARG.Core.Logging;

namespace YARG.Core.Chart
{
Expand Down Expand Up @@ -34,6 +37,99 @@ public static void ConvertFromTypeToType(this InstrumentDifficulty<GuitarNote> d
}
}

public static void ShuffleNotes(this InstrumentDifficulty<GuitarNote> difficulty, int seed)
{
var random = new Random(seed);
var activeNotes = new GuitarNote?[5 + 1]; // 5 frets, one open note

var currentNotes = new List<GuitarNote>();
var availableFrets = new List<int>();
var selectedFrets = new List<int>();

foreach (var parent in difficulty.Notes)
{
// Reset state
// Must be done first and not last, otherwise skipping a note won't reset state
currentNotes.Clear();
selectedFrets.Clear();
availableFrets.Clear();

// Clear no-longer-active notes
for (int i = 0; i < activeNotes.Length; i++)
{
if (activeNotes[i] is not {} activeNote)
continue; // Already inactive

bool isActive = activeNote.TickLength == 0
? activeNote.TickEnd == parent.Tick // If the note has no length, its end is inclusive
: activeNote.TickEnd > parent.Tick; // Otherwise, the end is exclusive

if (!isActive)
activeNotes[i] = null;
}

// Set up grab bag
if (activeNotes[1] == null) availableFrets.Add(1);
if (activeNotes[2] == null) availableFrets.Add(2);
if (activeNotes[3] == null) availableFrets.Add(3);
if (activeNotes[4] == null) availableFrets.Add(4);
if (activeNotes[5] == null) availableFrets.Add(5);
availableFrets.Shuffle(random);

// Pick a set of random notes for the chord
foreach (var note in parent.ChordEnumerator())
{
// Don't shuffle open notes
if (note.Fret == 0)
continue;

if (availableFrets.Count < 1)
{
// Ignore un-shuffleable notes
YargLogger.LogFormatWarning("Cannot shuffle note at {0:0.000} ({1}), removing.", note.Time, note.Tick);
continue;
}

int randomFret = availableFrets.PopRandom(random);
currentNotes.Add(note);
selectedFrets.Add(randomFret);
}

// Remove any notes that didn't make the cut
for (int i = 0; i < parent.ChildNotes.Count; i++)
{
var child = parent.ChildNotes[i];
if (!currentNotes.Contains(child))
parent.RemoveChildNote(child);
}

// Skip open notes and 5-note chords
if (currentNotes.Count < 1 || currentNotes.Count >= 5)
continue;

// Sort notes/frets to prepare for the next step
currentNotes.Sort((left, right) => left.Fret.CompareTo(right.Fret));
selectedFrets.Sort();

// Push all notes to the right, to prevent intermediate overlaps
for (int i = 0; i < currentNotes.Count; i++)
{
currentNotes[^(i + 1)].Fret = 5 - i;
}

// Apply shuffled frets
YargLogger.Assert(currentNotes.Count == selectedFrets.Count);
for (int i = 0; i < selectedFrets.Count; i++)
{
int fret = selectedFrets[i];
var note = currentNotes[i];

note.Fret = fret;
activeNotes[fret] = note;
}
}
}

public static void RemoveKickDrumNotes(this InstrumentDifficulty<DrumNote> difficulty)
{
var kickDrumPadIndex = difficulty.Instrument switch
Expand Down
11 changes: 11 additions & 0 deletions YARG.Core/Extensions/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ public static T PickRandom<T>(this List<T> list, Random random)
return list[random.Next(0, list.Count)];
}

/// <summary>
/// Picks and removes a random value from the list using the given random number generator.
/// </summary>
public static T PopRandom<T>(this List<T> list, Random random)
{
int index = random.Next(0, list.Count);
var value = list[index];
list.RemoveAt(index);
return value;
}

/// <summary>
/// Searches for an item in the list using the given search object and comparer function.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion YARG.Core/Game/Modifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public static Modifier PossibleModifiers(this GameMode gameMode)
Modifier.AllHopos |
Modifier.AllTaps |
Modifier.HoposToTaps |
Modifier.TapsToHopos,
Modifier.TapsToHopos |
Modifier.NoteShuffle,

GameMode.FourLaneDrums or
GameMode.FiveLaneDrums =>
Expand Down
10 changes: 8 additions & 2 deletions YARG.Core/Game/YargProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using YARG.Core.Chart;
using YARG.Core.Utility;
using YARG.Core.Extensions;
using YARG.Core.Song;

namespace YARG.Core.Game
{
Expand Down Expand Up @@ -50,7 +51,7 @@ public double InputCalibrationSeconds

/// <summary>
/// The difficulty to be saved in the profile.
///
///
/// If a song does not contain this difficulty, so long as the player
/// does not *explicitly* and *manually* change the difficulty, this value
/// should remain unchanged.
Expand Down Expand Up @@ -115,7 +116,7 @@ public void CopyModifiers(YargProfile profile)
CurrentModifiers = profile.CurrentModifiers;
}

public void ApplyModifiers<TNote>(InstrumentDifficulty<TNote> track) where TNote : Note<TNote>
public void ApplyModifiers<TNote>(SongEntry song, InstrumentDifficulty<TNote> track) where TNote : Note<TNote>
Comment on lines -118 to +119
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to pass in the SongEntry feels a little hacky, but I'm not sure how else to get the song hash in for note shuffle.

{
switch (CurrentInstrument.ToGameMode())
{
Expand Down Expand Up @@ -147,6 +148,11 @@ public void ApplyModifiers<TNote>(InstrumentDifficulty<TNote> track) where TNote
guitarTrack.ConvertFromTypeToType(GuitarNoteType.Tap, GuitarNoteType.Hopo);
}

if (IsModifierActive(Modifier.NoteShuffle))
{
guitarTrack.ShuffleNotes(unchecked(song.Hash.GetHashCode() * 133769420)); // heh
}

break;
case GameMode.FourLaneDrums:
case GameMode.FiveLaneDrums:
Expand Down