1
0
Fork 0

Implement seasonal content framework

to ensure that New Year's songs won't play in summer.
This commit is contained in:
ivan tkachenko 2026-01-11 02:52:26 +02:00
parent ebd7811b12
commit afb3e34e71
4 changed files with 102 additions and 5 deletions

View File

@ -7,6 +7,7 @@
- Added a new track Paarden.
- Added a new track DiscoKapot.
- Added an accessibility option to reduce the intensity of overly distracting visual effects.
- Seasonal content like New Year's songs (IkWilJe, Paarden, DiscoKapot) will only be available for selection during their respective seasons.
## MuzikaGromche 1337.9001.3 - v73 Happy New Year Edition

View File

@ -56,6 +56,7 @@ namespace MuzikaGromche
{
["Name"] = audioTrack.Name, // may be different from selectableTrack.Name, if selectable track is a group
["IsExplicit"] = selectableTrack.IsExplicit,
["Season"] = selectableTrack.Season?.Name,
["Language"] = selectableTrack.Language.Full,
["WindUpTimer"] = audioTrack.WindUpTimer,
["Bpm"] = audioTrack.Bpm,

View File

@ -884,6 +884,7 @@ namespace MuzikaGromche
Name = "IkWilJe",
AudioType = AudioType.OGGVORBIS,
Language = Language.ENGLISH,
Season = Season.NewYear,
WindUpTimer = 43.03f,
Beats = 13 * 4 + 2, // = 54
BeatsOffset = 0f,
@ -909,6 +910,7 @@ namespace MuzikaGromche
Name = "Paarden",
AudioType = AudioType.OGGVORBIS,
Language = Language.RUSSIAN,
Season = Season.NewYear,
WindUpTimer = 36.12f,
Bars = 8,
BeatsOffset = 0f,
@ -934,6 +936,7 @@ namespace MuzikaGromche
Name = "DiscoKapot",
AudioType = AudioType.OGGVORBIS,
Language = Language.RUSSIAN,
Season = Season.NewYear,
WindUpTimer = 30.3f,
Bars = 8,
BeatsOffset = 0f,
@ -970,12 +973,19 @@ namespace MuzikaGromche
public static ISelectableTrack ChooseTrack()
{
var seed = GetCurrentSeed();
var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
int[] weights = [.. tracks.Select(track => track.Weight.Value)];
var today = DateTime.Today;
var season = SeasonalContentManager.CurrentSeason(today);
var tracksEnumerable = SeasonalContentManager.Filter(Tracks, season);
if (Config.SkipExplicitTracks.Value)
{
tracksEnumerable = tracksEnumerable.Where(track => !track.IsExplicit);
}
var tracks = tracksEnumerable.ToArray();
int[] weights = tracks.Select(track => track.Weight.Value).ToArray();
var rwi = new RandomWeightedIndex(weights);
var trackId = rwi.GetRandomWeightedIndex(seed);
var track = tracks[trackId];
Log.LogInfo($"Seed is {seed}, chosen track is \"{track.Name}\", #{trackId} of {rwi}");
Log.LogInfo($"Seed is {seed}, season is {season?.Name ?? "<none>"}, chosen track is \"{track.Name}\", #{trackId} of {rwi}");
return tracks[trackId];
}
@ -1212,7 +1222,7 @@ namespace MuzikaGromche
// An instance of a track which appears as a configuration entry and
// can be selected using weighted random from a list of selectable tracks.
public interface ISelectableTrack
public interface ISelectableTrack : ISeasonalContent
{
// Name of the track, as shown in config entry UI; also used for default file names.
public string Name { get; init; }
@ -1423,6 +1433,7 @@ namespace MuzikaGromche
{
public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
public Season? Season { get; init; } = null;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
@ -1440,6 +1451,7 @@ namespace MuzikaGromche
public /* required */ string Name { get; init; } = "";
public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
public Season? Season { get; init; } = null;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
public /* required */ IAudioTrack[] Tracks = [];
@ -2480,8 +2492,9 @@ namespace MuzikaGromche
}
// Create slider entry for track
var seasonal = track.Season is Season season ? $"This is seasonal content for {season.Name}.\n\n" : "";
string warning = track.IsExplicit ? "Explicit Content/Lyrics!\n\n" : "";
string description = $"Language: {language.Full}\n\n{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track.";
string description = $"Language: {language.Full}\n\n{seasonal}{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track.";
track.Weight = configFile.Bind(
new ConfigDefinition(section, track.Name),
50,

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MuzikaGromche;
public delegate bool SeasonalContentPredicate(DateTime dateTime);
// I'm not really sure what to do with seasonal content yet.
//
// There could be two approaches:
// - Force seasonal content tracks to be the only available tracks during their season.
// Then seasons must be short, so they don't cut off too much content for too long.
// - Exclude seasonal content tracks from the pool when their season is not active.
// Considering how many tracks are there in the playlist permanently already,
// this might not give the seasonal content enough visibility.
//
// Either way, seasonal content tracks would be listed in the config UI at all times,
// which makes it confusing if you try to select only seasonal tracks outside of their season.
// Seasons may NOT overlap. There is at most ONE active season at any given date.
public readonly record struct Season(string Name, string Description, SeasonalContentPredicate IsActive)
{
public override string ToString() => Name;
public static readonly Season NewYear = new("New Year", "New Year and Christmas holiday season", dateTime =>
{
// December 10 - February 29
var month = dateTime.Month;
var day = dateTime.Day;
return (month == 12 && day >= 10) || (month == 1) || (month == 2 && day <= 29);
});
// Note: it is important that this property goes last
public static readonly Season[] All = [NewYear];
}
public interface ISeasonalContent
{
public Season? Season { get; init; }
}
public static class SeasonalContentManager
{
public static Season? CurrentSeason(DateTime dateTime)
{
foreach (var season in Season.All)
{
if (season.IsActive(dateTime))
{
return season;
}
}
return null;
}
public static Season? CurrentSeason() => CurrentSeason(DateTime.Today);
// Take second approach: filter out seasonal content that is not in the current season.
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items, Season? season) where T : ISeasonalContent
{
return items.Where(item =>
{
if (item.Season == null)
{
return true; // always available
}
return item.Season == season;
});
}
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items, DateTime dateTime) where T : ISeasonalContent
{
var season = CurrentSeason(dateTime);
return Filter(items, season);
}
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items) where T : ISeasonalContent
{
return Filter(items, DateTime.Today);
}
}