diff --git a/.gitignore b/.gitignore index c301ca8..bb88804 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,37 @@ -# Godot 4.x -.godot/ -.idea/ -sdk/ -bin/ -obj/ - -#ai -.omo/ -.opencode/ - -gen/ -obj/ -bin/ - -*.tres~ -*.tscn~ - -# GDScript LSP -.godot/ - -# Export -*.exe -*.dmg -*.apk -*.aab -*.pck -*.zip -*.log - -# OS -.DS_Store -Thumbs.db - -# Editor -*.import -*.godot.uid +# Godot 4.x +.godot/ +.idea/ +sdk/ +bin/ +obj/ + +#ai +.omo/ +.opencode/ + +gen/ +obj/ +bin/ + +*.tres~ +*.tscn~ + +# GDScript LSP +.godot/ + +# Export +*.exe +*.dmg +*.apk +*.aab +*.pck +*.zip +*.log + +# OS +.DS_Store +Thumbs.db + +# Editor +*.import +*.godot.uid diff --git a/manifest.json b/manifest.json index b4a158e..ecf29c2 100644 --- a/manifest.json +++ b/manifest.json @@ -8,6 +8,7 @@ "sources": [ "src/**/*.cs" ], + "test_sources": ["tests/*.cs"], "depends": ["shell2d_base>=1", "ui_base>=1"], "ui": [] } diff --git a/src/MusicPlayerBehaviour.cs b/src/MusicPlayerBehaviour.cs index 0e788a2..20518a7 100644 --- a/src/MusicPlayerBehaviour.cs +++ b/src/MusicPlayerBehaviour.cs @@ -276,24 +276,11 @@ namespace Cthangover.Mods.Audio if (dict == null || !dict.TryGetValue(playlistContext.LastMusicType, out var list) || Lists.IsEmpty(list)) return; - int iteration = 0; - for (;;) - { - int index = (int)(GD.Randi() % (uint)list.Count); - var nextMusic = list[index]; + var nextMusic = TrackPicker.Next(list, playlistContext.LastMusicName, System.Random.Shared); + GameLogger.Log("AUDIO", $"NextSound -> '{nextMusic}' (type={playlistContext.LastMusicType}, scene={playlistContext.Playlist?.Scene})"); - if (list.Count == 1 || playlistContext.LastMusicName != nextMusic) - { - GameLogger.Log("AUDIO", $"NextSound -> '{nextMusic}' (type={playlistContext.LastMusicType}, scene={playlistContext.Playlist?.Scene})"); - - StopMusic(); - PlayMusic(nextMusic); - break; - } - - if (iteration++ > 10) - break; - } + StopMusic(); + PlayMusic(nextMusic); } /// diff --git a/src/TrackPicker.cs b/src/TrackPicker.cs new file mode 100644 index 0000000..10a45e1 --- /dev/null +++ b/src/TrackPicker.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Cthangover.Mods.Audio +{ + /// + /// Picks the next track of a music bucket: a random one different from the + /// last played when the bucket offers another, otherwise the last one + /// again. Choosing among the other tracks directly — instead of rerolling + /// until the pick differs — cannot give up and leave the player silent + /// (a bucket holding copies of one track exhausted every reroll). + /// + public static class TrackPicker + { + /// The next track of , or null for an empty bucket. + public static string Next(IReadOnlyList tracks, string last, Random random) + { + if (tracks == null || tracks.Count == 0) + return null; + + var others = new List(); + foreach (var track in tracks) + { + if (track != last) + others.Add(track); + } + + return others.Count > 0 ? others[random.Next(others.Count)] : tracks[0]; + } + } +} diff --git a/tests/TrackPickerTests.cs b/tests/TrackPickerTests.cs new file mode 100644 index 0000000..f2903dd --- /dev/null +++ b/tests/TrackPickerTests.cs @@ -0,0 +1,27 @@ +namespace Cthangover.Mods.Audio.Tests +{ + using System; + using System.Collections.Generic; + using Cthangover.Core.Mods.Testing; + + /// + /// Choosing the next track never fails: it avoids repeating the last one + /// when anything else is available, and a bucket made of one track (or of + /// copies of it) still plays instead of falling silent. + /// + public class TrackPickerTests + { + [ModTest("music/next_track_avoids_repeat_and_never_gives_up")] + static void NextTrackAvoidsRepeat(ModTestContext ctx) + { + var random = new Random(3); + var list = new List { "a", "b" }; + for (var i = 0; i < 200; i++) + ModAssert.Equal("b", TrackPicker.Next(list, "a", random), "the other track"); + + ModAssert.Equal("a", TrackPicker.Next(new List { "a", "a", "a" }, "a", random), "copies of one track still play"); + ModAssert.Equal("a", TrackPicker.Next(new List { "a" }, "a", random), "a single track repeats"); + ModAssert.Null(TrackPicker.Next(new List(), "a", random), "an empty bucket gives nothing"); + } + } +}