1
0
Fork 0

Compare commits

..

No commits in common. "6a9ea8d4afdd120385cd99339f4ed27e8cbb1925" and "df796965f2b9f77a64d03955a5d3387bbf5ed675" have entirely different histories.

24 changed files with 122 additions and 370 deletions

BIN
Assets/Beha1Intro.ogg (Stored with Git LFS)

Binary file not shown.

BIN
Assets/Beha2Intro.ogg (Stored with Git LFS)

Binary file not shown.

BIN
Assets/Beha3Intro.ogg (Stored with Git LFS)

Binary file not shown.

BIN
Assets/BehaLoop.ogg (Stored with Git LFS)

Binary file not shown.

View File

@ -1,15 +1,10 @@
# Changelog # Changelog
## MuzikaGromche 1337.420.9001 - Multiverse Edition
- Added support for tracks to rotate between multiple audio variants during a round.
- Added a new track Beha with three different variants of intro.
## MuzikaGromche 1337.420.69 - It's All DiscoNnected Edition ## MuzikaGromche 1337.420.69 - It's All DiscoNnected Edition
- Fixed harmless but annoying errors in BepInEx console output. - Fix harmless but annoying errors in BepInEx console output.
- Improve smoothness of color animations. - Improve smoothness of color animations.
- Added a new track BeefLiver. - Add a new track.
## MuzikaGromche 1337.69.420 - It's All Connected Edition ## MuzikaGromche 1337.69.420 - It's All Connected Edition

View File

@ -8,7 +8,7 @@
<AssemblyName>Ratijas.MuzikaGromche</AssemblyName> <AssemblyName>Ratijas.MuzikaGromche</AssemblyName>
<Product>Muzika Gromche</Product> <Product>Muzika Gromche</Product>
<Description>Add some content to your inverse teleporter experience on Titan!</Description> <Description>Add some content to your inverse teleporter experience on Titan!</Description>
<Version>1337.420.9001</Version> <Version>1337.420.69</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -110,7 +110,7 @@
--> -->
<Target Name="wav2ogg"> <Target Name="wav2ogg">
<ItemGroup> <ItemGroup>
<TrackNames Include="$(TrackName)Intro" /> <TrackNames Include="$(TrackName)Start" />
<TrackNames Include="$(TrackName)Loop" /> <TrackNames Include="$(TrackName)Loop" />
</ItemGroup> </ItemGroup>
<Exec Command="ffmpeg -bitexact -y -i $(WavExportDir)%(TrackNames.Identity).wav $(SolutionDir)Assets\%(TrackNames.Identity).ogg" /> <Exec Command="ffmpeg -bitexact -y -i $(WavExportDir)%(TrackNames.Identity).wav $(SolutionDir)Assets\%(TrackNames.Identity).ogg" />

View File

@ -48,8 +48,8 @@ namespace MuzikaGromche
.Select(a => $" Trying... {a}") .Select(a => $" Trying... {a}")
]; ];
public static readonly ISelectableTrack[] Tracks = [ public static readonly Track[] Tracks = [
new SelectableAudioTrack new Track
{ {
Name = "MuzikaGromche", Name = "MuzikaGromche",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -89,7 +89,7 @@ namespace MuzikaGromche
(63, "Muzyka Gromche\nGlaza zakryty >_<"), (63, "Muzyka Gromche\nGlaza zakryty >_<"),
], ],
}, },
new SelectableAudioTrack new Track
{ {
Name = "VseVZale", Name = "VseVZale",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -124,7 +124,7 @@ namespace MuzikaGromche
(60, "Everybody shake your body"), (60, "Everybody shake your body"),
], ],
}, },
new SelectableAudioTrack new Track
{ {
Name = "DeployDestroy", Name = "DeployDestroy",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -167,7 +167,7 @@ namespace MuzikaGromche
(25, "Davaj-davaj!"), (25, "Davaj-davaj!"),
], ],
}, },
new SelectableAudioTrack new Track
{ {
Name = "MoyaZhittya", Name = "MoyaZhittya",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -214,7 +214,7 @@ namespace MuzikaGromche
( 30, "IT'S MY"), ( 30, "IT'S MY"),
], ],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Gorgorod", Name = "Gorgorod",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -232,7 +232,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [20], FlickerLightsTimeSeries = [20],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Durochka", Name = "Durochka",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -250,7 +250,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-9], FlickerLightsTimeSeries = [-9],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "ZmeiGorynich", Name = "ZmeiGorynich",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -268,7 +268,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5, 31], FlickerLightsTimeSeries = [-5, 31],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "GodMode", Name = "GodMode",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -286,7 +286,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5], FlickerLightsTimeSeries = [-5],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "RiseAndShine", Name = "RiseAndShine",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -304,7 +304,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5.5f, 31, 63.9f], FlickerLightsTimeSeries = [-5.5f, 31, 63.9f],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Song2", Name = "Song2",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -322,7 +322,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [2.5f], FlickerLightsTimeSeries = [2.5f],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Peretasovka", Name = "Peretasovka",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -340,7 +340,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-8, 31], FlickerLightsTimeSeries = [-8, 31],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Yalgaar", Name = "Yalgaar",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -358,7 +358,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5], FlickerLightsTimeSeries = [-5],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Chereshnya", Name = "Chereshnya",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -379,7 +379,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-5, 27, 29, 59, 61], FlickerLightsTimeSeries = [-5, 27, 29, 59, 61],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "PWNED", Name = "PWNED",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -451,7 +451,7 @@ namespace MuzikaGromche
(98, $"\t\t\tresolving ur private IP\nP_WNED"), (98, $"\t\t\tresolving ur private IP\nP_WNED"),
], ],
}, },
new SelectableAudioTrack new Track
{ {
Name = "Kach", Name = "Kach",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -475,7 +475,7 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45], FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45],
Lyrics = [], Lyrics = [],
}, },
new SelectableAudioTrack new Track
{ {
Name = "BeefLiver", Name = "BeefLiver",
AudioType = AudioType.OGGVORBIS, AudioType = AudioType.OGGVORBIS,
@ -496,78 +496,9 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-48, -40, -4.5f, 44], FlickerLightsTimeSeries = [-48, -40, -4.5f, 44],
Lyrics = [], Lyrics = [],
}, },
new SelectableTracksGroup
{
Name = "Beha",
Language = Language.RUSSIAN,
IsExplicit = true,
Tracks =
[
new CoreAudioTrack
{
Name = "Beha1",
FileNameLoop = "BehaLoop.ogg",
AudioType = AudioType.OGGVORBIS,
WindUpTimer = 35.23f,
Beats = 8 * 4 + 2,
BeatsOffset = 0.0f,
ColorTransitionIn = 0.1f,
ColorTransitionOut = 0.6f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse([
"#9554F9", "#3769FD", "#E43B65", "#59CFEA", "#7F3FEE", "#C831FE",
]),
LoopOffset = 0,
FadeOutBeat = -4,
FadeOutDuration = 3.9f,
FlickerLightsTimeSeries = [-6, 16.5f],
Lyrics = [],
},
new CoreAudioTrack
{
Name = "Beha2",
FileNameLoop = "BehaLoop.ogg",
AudioType = AudioType.OGGVORBIS,
WindUpTimer = 38.16f,
Beats = 8 * 4 + 2,
BeatsOffset = 0.0f,
ColorTransitionIn = 0.1f,
ColorTransitionOut = 0.6f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse([
"#9554F9", "#3769FD", "#E43B65", "#59CFEA", "#7F3FEE", "#C831FE",
]),
LoopOffset = 0,
FadeOutBeat = -4,
FadeOutDuration = 3.9f,
FlickerLightsTimeSeries = [-6, 16.5f],
Lyrics = [],
},
new CoreAudioTrack
{
Name = "Beha3",
FileNameLoop = "BehaLoop.ogg",
AudioType = AudioType.OGGVORBIS,
WindUpTimer = 35.21f,
Beats = 8 * 4 + 2,
BeatsOffset = 0.0f,
ColorTransitionIn = 0.1f,
ColorTransitionOut = 0.6f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse([
"#9554F9", "#3769FD", "#E43B65", "#59CFEA", "#7F3FEE", "#C831FE",
]),
LoopOffset = 0,
FadeOutBeat = -4,
FadeOutDuration = 3.9f,
FlickerLightsTimeSeries = [-6, 16.5f],
Lyrics = [],
},
],
},
]; ];
public static ISelectableTrack ChooseTrack() public static Track ChooseTrack()
{ {
var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed; var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed;
var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks; var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
@ -579,12 +510,12 @@ namespace MuzikaGromche
return tracks[trackId]; return tracks[trackId];
} }
public static IAudioTrack? FindTrackNamed(string name) public static Track? FindTrackNamed(string name)
{ {
return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name); return Tracks.FirstOrDefault(track => track.Name == name);
} }
internal static IAudioTrack? CurrentTrack; internal static Track? CurrentTrack;
internal static BeatTimeState? BeatTimeState; internal static BeatTimeState? BeatTimeState;
public static void SetLightColor(Color color) public static void SetLightColor(Color color)
@ -628,49 +559,29 @@ namespace MuzikaGromche
Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks); Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks);
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> requests = []; UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2];
requests.EnsureCapacity(Tracks.Length * 2); for (int i = 0; i < Tracks.Length; i++)
foreach (var track in Tracks.SelectMany(track => track.GetTracks()))
{ {
foreach (var (fileName, setter) in new (string, Action<AudioClip>)[] Track track = Tracks[i];
{ requests[i * 2] = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{track.FileNameStart}", track.AudioType);
(track.FileNameIntro, clip => track.LoadedIntro = clip), requests[i * 2 + 1] = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{track.FileNameLoop}", track.AudioType);
(track.FileNameLoop, clip => track.LoadedLoop = clip), requests[i * 2].SendWebRequest();
}) requests[i * 2 + 1].SendWebRequest();
{
if (requests.TryGetValue(fileName, out var tuple))
{
tuple.Setters.Add(setter);
}
else
{
var request = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{fileName}", track.AudioType);
request.SendWebRequest();
requests[fileName] = (request, [setter]);
}
}
} }
while (!requests.Values.All(tuple => tuple.Request.isDone)) { } while (!requests.All(request => request.isDone)) { }
if (requests.Values.All(tuple => tuple.Request.result == UnityWebRequest.Result.Success)) if (requests.All(request => request.result == UnityWebRequest.Result.Success))
{ {
for (int i = 0; i < Tracks.Length; i++)
foreach (var (fileName, tuple) in requests)
{ {
var clip = DownloadHandlerAudioClip.GetContent(tuple.Request); Track track = Tracks[i];
foreach (var setter in tuple.Setters) track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]);
{ track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]);
setter(clip);
}
}
#if DEBUG #if DEBUG
foreach (var track in Tracks) Debug.Log($"{nameof(MuzikaGromche)} Track {track.Name} {track.LoadedStart.length:N4} {track.LoadedLoop.length:N4}");
{
track.Debug();
}
#endif #endif
}
Config = new Config(base.Config); Config = new Config(base.Config);
DiscoBallManager.Load(); DiscoBallManager.Load();
PoweredLightsAnimators.Load(); PoweredLightsAnimators.Load();
@ -687,7 +598,7 @@ namespace MuzikaGromche
} }
else else
{ {
var failed = requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl()); var failed = requests.Where(request => request.result != UnityWebRequest.Result.Success).Select(request => request.GetUrl());
Logger.LogError("Could not load audio file " + string.Join(", ", failed)); Logger.LogError("Could not load audio file " + string.Join(", ", failed));
} }
} }
@ -790,80 +701,48 @@ namespace MuzikaGromche
} }
} }
public struct SelectableTrackData() public class Track
{ {
// Name of the track, as shown in config entry UI; also used for default file names. public required string Name;
public required string Name { get; init; }
// Language of the track's lyrics. // Language of the track's lyrics.
public required Language Language { get; init; } public required Language Language;
// Whether this track has NSFW/explicit lyrics. // Whether this track has NSFW/explicit lyrics.
public bool IsExplicit { get; init; } = false; public bool IsExplicit = false;
// Wind-up time can and should be shorter than the Start audio track,
// How often this track should be chosen, relative to the sum of weights of all tracks. // so that the "pop" effect can be baked into the Start audio and kept away
public ConfigEntry<int> Weight { get; internal set; } = null!;
}
// 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
{
// Name of the track, as shown in config entry UI; also used for default file names.
public string Name { get; init; }
// Language of the track's lyrics.
public Language Language { get; init; }
// Whether this track has NSFW/explicit lyrics.
public bool IsExplicit { get; init; }
// How often this track should be chosen, relative to the sum of weights of all tracks.
internal ConfigEntry<int> Weight { get; set; }
internal IAudioTrack[] GetTracks();
// Index is a non-negative monotonically increasing number of times
// this ISelectableTrack has been played for this Jester on this day.
// A group of tracks can use this index to rotate tracks sequentially.
internal IAudioTrack SelectTrack(int index);
internal void Debug();
}
// An instance of a track which has file names, timings data, palette; can be loaded and played.
public interface IAudioTrack
{
// Name of the track used for default file names.
public string Name { get; }
// Wind-up time can and should be shorter than the Intro audio track,
// so that the "pop" effect can be baked into the Intro audio and kept away
// from the looped part. This also means that the light show starts before // from the looped part. This also means that the light show starts before
// the looped track does, so we need to sync them up as soon as we enter the Loop. // the looped track does, so we need to sync them up as soon as we enter the Loop.
public float WindUpTimer { get; } public required float WindUpTimer;
// Estimated number of beats per minute. Not used for light show, but might come in handy. // Estimated number of beats per minute. Not used for light show, but might come in handy.
public float Bpm => 60f / (LoadedLoop.length / Beats); public float Bpm => 60f / (LoadedLoop.length / Beats);
// How many beats the loop segment has. The default strategy is to switch color of lights on each beat. // How many beats the loop segment has. The default strategy is to switch color of lights on each beat.
public int Beats { get; } public int Beats;
// Number of beats between WindUpTimer and where looped segment starts (not the loop audio). // Number of beats between WindUpTimer and where looped segment starts (not the loop audio).
public int LoopOffset { get; } public int LoopOffset = 0;
public float LoopOffsetInSeconds => LoopOffset / Beats * LoadedLoop.length; public float LoopOffsetInSeconds => LoopOffset / Beats * LoadedLoop.length;
// Shorthand for four beats
public int Bars
{
set => Beats = value * 4;
}
// MPEG is basically mp3, and it can produce gaps at the start. // MPEG is basically mp3, and it can produce gaps at the start.
// WAV is OK, but takes a lot of space. Try OGGVORBIS instead. // WAV is OK, but takes a lot of space. Try OGGVORBIS instead.
public AudioType AudioType { get; } public AudioType AudioType = AudioType.MPEG;
public AudioClip LoadedIntro { get; internal set; } public AudioClip LoadedStart = null!;
public AudioClip LoadedLoop { get; internal set; } public AudioClip LoadedLoop = null!;
public string FileNameIntro { get; } // How often this track should be chosen, relative to the sum of weights of all tracks.
public string FileNameLoop { get; } public ConfigEntry<int> Weight = null!;
public string Ext => AudioType switch public string FileNameStart => $"{Name}Start.{Ext}";
public string FileNameLoop => $"{Name}Loop.{Ext}";
private string Ext => AudioType switch
{ {
AudioType.MPEG => "mp3", AudioType.MPEG => "mp3",
AudioType.WAV => "wav", AudioType.WAV => "wav",
@ -872,86 +751,28 @@ namespace MuzikaGromche
}; };
// Offset of beats. Bigger offset => colors will change later. // Offset of beats. Bigger offset => colors will change later.
public float BeatsOffset { get; }
// Offset of beats, in seconds. Bigger offset => colors will change later.
public float BeatsOffsetInSeconds => BeatsOffset / Beats * LoadedLoop.length;
public float FadeOutBeat { get; }
public float FadeOutDuration { get; }
// Duration of color transition, measured in beats.
public float ColorTransitionIn { get; }
public float ColorTransitionOut { get; }
// Easing function for color transitions.
public Easing ColorTransitionEasing { get; }
public float[] FlickerLightsTimeSeries { get; }
public float[] LyricsTimeSeries { get; }
// Lyrics line may contain multiple tab-separated alternatives.
// In such case, a random number chosen and updated once per loop
// is used to select an alternative.
// If the chosen alternative is an empty string, lyrics event shall be skipped.
public string[] LyricsLines { get; }
public Palette Palette { get; }
}
// Core audio track implementation with some defaults and config overrides.
// Suitable to declare elemnents of SelectableTracksGroup and as a base for standalone selectable tracks.
public class CoreAudioTrack : IAudioTrack
{
public required string Name { get; init; }
public required float WindUpTimer { get; init; }
public int Beats { get; init; }
// Shorthand for four beats
public int Bars
{
init => Beats = value * 4;
}
public int LoopOffset { get; init; } = 0;
public AudioType AudioType { get; init; } = AudioType.MPEG;
public AudioClip LoadedIntro { get; set; } = null!;
public AudioClip LoadedLoop { get; set; } = null!;
private string? FileNameIntroOverride = null;
public string FileNameIntro
{
get => FileNameIntroOverride ?? $"{Name}Intro.{((IAudioTrack)this).Ext}";
init => FileNameIntroOverride = value;
}
private string? FileNameLoopOverride = null;
public string FileNameLoop
{
get => FileNameLoopOverride ?? $"{Name}Loop.{((IAudioTrack)this).Ext}";
init => FileNameLoopOverride = value;
}
public float _BeatsOffset = 0f; public float _BeatsOffset = 0f;
public float BeatsOffset public float BeatsOffset
{ {
get => Config.BeatsOffsetOverride ?? _BeatsOffset; get => Config.BeatsOffsetOverride ?? _BeatsOffset;
init => _BeatsOffset = value; set => _BeatsOffset = value;
} }
// Offset of beats, in seconds. Bigger offset => colors will change later.
public float BeatsOffsetInSeconds => BeatsOffset / Beats * LoadedLoop.length;
public float _FadeOutBeat = float.NaN; public float _FadeOutBeat = float.NaN;
public float FadeOutBeat public float FadeOutBeat
{ {
get => Config.FadeOutBeatOverride ?? _FadeOutBeat; get => Config.FadeOutBeatOverride ?? _FadeOutBeat;
init => _FadeOutBeat = value; set => _FadeOutBeat = value;
} }
public float _FadeOutDuration = 2f; public float _FadeOutDuration = 2f;
public float FadeOutDuration public float FadeOutDuration
{ {
get => Config.FadeOutDurationOverride ?? _FadeOutDuration; get => Config.FadeOutDurationOverride ?? _FadeOutDuration;
init => _FadeOutDuration = value; set => _FadeOutDuration = value;
} }
// Duration of color transition, measured in beats. // Duration of color transition, measured in beats.
@ -959,14 +780,14 @@ namespace MuzikaGromche
public float ColorTransitionIn public float ColorTransitionIn
{ {
get => Config.ColorTransitionInOverride ?? _ColorTransitionIn; get => Config.ColorTransitionInOverride ?? _ColorTransitionIn;
init => _ColorTransitionIn = value; set => _ColorTransitionIn = value;
} }
public float _ColorTransitionOut = 0.25f; public float _ColorTransitionOut = 0.25f;
public float ColorTransitionOut public float ColorTransitionOut
{ {
get => Config.ColorTransitionOutOverride ?? _ColorTransitionOut; get => Config.ColorTransitionOutOverride ?? _ColorTransitionOut;
init => _ColorTransitionOut = value; set => _ColorTransitionOut = value;
} }
// Easing function for color transitions. // Easing function for color transitions.
@ -976,14 +797,14 @@ namespace MuzikaGromche
get => Config.ColorTransitionEasingOverride != null get => Config.ColorTransitionEasingOverride != null
? Easing.FindByName(Config.ColorTransitionEasingOverride) ? Easing.FindByName(Config.ColorTransitionEasingOverride)
: _ColorTransitionEasing; : _ColorTransitionEasing;
init => _ColorTransitionEasing = value; set => _ColorTransitionEasing = value;
} }
public float[] _FlickerLightsTimeSeries = []; public float[] _FlickerLightsTimeSeries = [];
public float[] FlickerLightsTimeSeries public float[] FlickerLightsTimeSeries
{ {
get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries; get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries;
init set
{ {
Array.Sort(value); Array.Sort(value);
_FlickerLightsTimeSeries = value; _FlickerLightsTimeSeries = value;
@ -1024,53 +845,6 @@ namespace MuzikaGromche
} }
} }
// Standalone, top-level, selectable audio track
public class SelectableAudioTrack : CoreAudioTrack, ISelectableTrack
{
public required Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
IAudioTrack ISelectableTrack.SelectTrack(int index) => this;
void ISelectableTrack.Debug()
{
Debug.Log($"{nameof(MuzikaGromche)} Track \"{Name}\", Intro={LoadedIntro.length:N4}, Loop={LoadedLoop.length:N4}");
}
}
public class SelectableTracksGroup : ISelectableTrack
{
public required string Name { get; init; }
public required Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
public required IAudioTrack[] Tracks;
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
IAudioTrack ISelectableTrack.SelectTrack(int index)
{
if (Tracks.Length == 0)
{
throw new IndexOutOfRangeException("Tracks list is empty");
}
return Mod.Index(Tracks, index);
}
void ISelectableTrack.Debug()
{
Debug.Log($"{nameof(MuzikaGromche)} Track Group \"{Name}\", Count={Tracks.Length}");
foreach (var (track, index) in Tracks.Select((x, i) => (x, i)))
{
Debug.Log($"{nameof(MuzikaGromche)} Track {index} \"{track.Name}\", Intro={track.LoadedIntro.length:N4}, Loop={track.LoadedLoop.length:N4}");
}
}
}
readonly record struct BeatTimestamp readonly record struct BeatTimestamp
{ {
// Number of beats in the loop audio segment. // Number of beats in the loop audio segment.
@ -1325,6 +1099,11 @@ namespace MuzikaGromche
} }
} }
public void Finish()
{
IsPlaying = false;
}
public override string ToString() public override string ToString()
{ {
return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} " return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} "
@ -1337,36 +1116,36 @@ namespace MuzikaGromche
class JesterAudioSourcesState class JesterAudioSourcesState
{ {
private readonly float IntroClipLength; private readonly float StartClipLength;
// Neither intro.isPlaying or loop.isPlaying are reliable indicators of which track is actually playing right now: // Neither start.isPlaying or loop.isPlaying are reliable indicators of which track is actually playing right now:
// intro.isPlaying would be true during the loop when Jester chases a player, // start.isPlaying would be true during the loop when Jester chases a player,
// loop.isPlaying would be true when it is played delyaed but hasn't actually started playing yet. // loop.isPlaying would be true when it is played delyaed but hasn't actually started playing yet.
private readonly ExtrapolatedAudioSourceState Intro = new(); private readonly ExtrapolatedAudioSourceState Start = new();
private readonly ExtrapolatedAudioSourceState Loop = new(); private readonly ExtrapolatedAudioSourceState Loop = new();
// If true, use Start state as a reference, otherwise use Loop. // If true, use Start state as a reference, otherwise use Loop.
private bool ReferenceIsIntro = true; private bool ReferenceIsStart = true;
public bool HasStarted => Intro.HasStarted; public bool HasStarted => Start.HasStarted;
public bool IsExtrapolated => ReferenceIsIntro ? Intro.IsExtrapolated : Loop.IsExtrapolated; public bool IsExtrapolated => ReferenceIsStart ? Start.IsExtrapolated : Loop.IsExtrapolated;
// Time from the start of the start clip. It wraps when the loop AudioSource loops: // Time from the start of the start clip. It wraps when the loop AudioSource loops:
// [...start...][...loop...] // [...start...][...loop...]
// ^ | // ^ |
// `----------' // `----------'
public float Time => ReferenceIsIntro public float Time => ReferenceIsStart
? Intro.Time ? Start.Time
: IntroClipLength + Loop.Time; : StartClipLength + Loop.Time;
public JesterAudioSourcesState(float introClipLength) public JesterAudioSourcesState(float startClipLength)
{ {
IntroClipLength = introClipLength; StartClipLength = startClipLength;
} }
public void Update(AudioSource intro, AudioSource loop, float realtime) public void Update(AudioSource start, AudioSource loop, float realtime)
{ {
// It doesn't make sense to update start state after loop has started (because start.isPlaying occasionally becomes true). // It doesn't make sense to update start state after loop has started (because start.isPlaying occasionally becomes true).
// But always makes sense to update loop, so we can check if it has actually started. // But always makes sense to update loop, so we can check if it has actually started.
@ -1375,13 +1154,13 @@ namespace MuzikaGromche
if (!Loop.HasStarted) if (!Loop.HasStarted)
{ {
#if DEBUG #if DEBUG
Debug.Assert(ReferenceIsIntro); Debug.Assert(ReferenceIsStart);
#endif #endif
Intro.Update(intro, realtime); Start.Update(start, realtime);
} }
else else
{ {
ReferenceIsIntro = false; ReferenceIsStart = false;
} }
} }
} }
@ -1417,7 +1196,7 @@ namespace MuzikaGromche
// //
// NOTE 1: PlayDelayed also counts as isPlaying, so loop.isPlaying is always true and as such it's useful. // NOTE 1: PlayDelayed also counts as isPlaying, so loop.isPlaying is always true and as such it's useful.
// NOTE 2: There is a weird state when Jester has popped and chases a player: // NOTE 2: There is a weird state when Jester has popped and chases a player:
// Intro/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that. // Start/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that.
var offset = StartOfLoop + additionalOffset; var offset = StartOfLoop + additionalOffset;
@ -1446,7 +1225,7 @@ namespace MuzikaGromche
class BeatTimeState class BeatTimeState
{ {
private readonly IAudioTrack track; private readonly Track track;
private readonly JesterAudioSourcesState AudioState; private readonly JesterAudioSourcesState AudioState;
@ -1464,7 +1243,7 @@ namespace MuzikaGromche
private bool WindUpZeroBeatEventTriggered = false; private bool WindUpZeroBeatEventTriggered = false;
public BeatTimeState(IAudioTrack track) public BeatTimeState(Track track)
{ {
if (LyricsRandom == null) if (LyricsRandom == null)
{ {
@ -1472,15 +1251,15 @@ namespace MuzikaGromche
LyricsRandomPerLoop = LyricsRandom.Next(); LyricsRandomPerLoop = LyricsRandom.Next();
} }
this.track = track; this.track = track;
AudioState = new(track.LoadedIntro.length); AudioState = new(track.LoadedStart.length);
WindUpLoopingState = new(track.WindUpTimer, track.LoadedLoop.length, track.Beats); WindUpLoopingState = new(track.WindUpTimer, track.LoadedLoop.length, track.Beats);
LoopLoopingState = new(track.WindUpTimer + track.LoopOffsetInSeconds, track.LoadedLoop.length, track.Beats); LoopLoopingState = new(track.WindUpTimer + track.LoopOffsetInSeconds, track.LoadedLoop.length, track.Beats);
} }
public List<BaseEvent> Update(AudioSource intro, AudioSource loop) public List<BaseEvent> Update(AudioSource start, AudioSource loop)
{ {
var time = Time.realtimeSinceStartup; var time = Time.realtimeSinceStartup;
AudioState.Update(intro, loop, time); AudioState.Update(start, loop, time);
if (AudioState.HasStarted) if (AudioState.HasStarted)
{ {
@ -1998,7 +1777,7 @@ namespace MuzikaGromche
private void SetupEntriesToSkipWinding(ConfigFile configFile) private void SetupEntriesToSkipWinding(ConfigFile configFile)
{ {
var syncedEntry = configFile.BindSyncedEntry("General", "Skip Winding Phase", false, var syncedEntry = configFile.BindSyncedEntry("General", "Skip Winding Phase", false,
new ConfigDescription("Skip most of the wind-up/intro music.\n\nUse this option to test your Loop audio segment.")); new ConfigDescription("Skip most of the wind-up/intro/start music.\n\nUse this option to test your Loop audio segment."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(syncedEntry.Entry, Default(new BoolCheckBoxOptions()))); LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(syncedEntry.Entry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(syncedEntry); CSyncHackAddSyncedEntry(syncedEntry);
syncedEntry.Changed += (sender, args) => apply(); syncedEntry.Changed += (sender, args) => apply();
@ -2047,7 +1826,7 @@ namespace MuzikaGromche
void load() void load()
{ {
var palette = (Plugin.CurrentTrack as CoreAudioTrack)?._Palette ?? Palette.DEFAULT; var palette = Plugin.CurrentTrack?._Palette ?? Palette.DEFAULT;
var colors = palette.Colors; var colors = palette.Colors;
var count = Math.Min(colors.Count(), maxCustomPaletteSize); var count = Math.Min(colors.Count(), maxCustomPaletteSize);
@ -2080,7 +1859,7 @@ namespace MuzikaGromche
const string section = "Timings"; const string section = "Timings";
var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f); var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f);
// Declare and initialize early to avoid "Use of unassigned local variable" // Declare and initialize early to avoid "Use of unassigned local variable"
List<(Action<CoreAudioTrack?> Load, Action Apply)> entries = []; List<(Action<Track?> Load, Action Apply)> entries = [];
SyncedEntry<bool> overrideTimingsSyncedEntry = null!; SyncedEntry<bool> overrideTimingsSyncedEntry = null!;
SyncedEntry<float> fadeOutBeatSyncedEntry = null!; SyncedEntry<float> fadeOutBeatSyncedEntry = null!;
SyncedEntry<float> fadeOutDurationSyncedEntry = null!; SyncedEntry<float> fadeOutDurationSyncedEntry = null!;
@ -2139,12 +1918,12 @@ namespace MuzikaGromche
registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = x); registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = x);
registerClass(colorTransitionEasingSyncedEntry, t => t._ColorTransitionEasing.Name, x => ColorTransitionEasingOverride = x); registerClass(colorTransitionEasingSyncedEntry, t => t._ColorTransitionEasing.Name, x => ColorTransitionEasingOverride = x);
void register<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action applier) void register<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action applier)
{ {
CSyncHackAddSyncedEntry(syncedEntry); CSyncHackAddSyncedEntry(syncedEntry);
syncedEntry.SyncHostToLocal(); syncedEntry.SyncHostToLocal();
syncedEntry.Changed += (sender, args) => applier(); syncedEntry.Changed += (sender, args) => applier();
void loader(CoreAudioTrack? track) void loader(Track? track)
{ {
// if track is null, set everything to defaults // if track is null, set everything to defaults
syncedEntry.LocalValue = track == null ? (T)syncedEntry.Entry.DefaultValue : getter(track); syncedEntry.LocalValue = track == null ? (T)syncedEntry.Entry.DefaultValue : getter(track);
@ -2152,11 +1931,11 @@ namespace MuzikaGromche
entries.Add((loader, applier)); entries.Add((loader, applier));
} }
void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : struct => void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action<T?> setter) where T : struct =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null)); register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null));
void registerClass<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : class => void registerClass<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action<T?> setter) where T : class =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null)); register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null));
void registerArray<T>(SyncedEntry<string> syncedEntry, Func<CoreAudioTrack, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct => void registerArray<T>(SyncedEntry<string> syncedEntry, Func<Track, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct =>
register(syncedEntry, register(syncedEntry,
(track) => string.Join(", ", getter(track)), (track) => string.Join(", ", getter(track)),
() => () =>
@ -2190,7 +1969,7 @@ namespace MuzikaGromche
var track = Plugin.CurrentTrack; var track = Plugin.CurrentTrack;
foreach (var entry in entries) foreach (var entry in entries)
{ {
entry.Load(track as CoreAudioTrack); entry.Load(track);
} }
} }
@ -2237,11 +2016,6 @@ namespace MuzikaGromche
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
{ {
// Number of times a selected track has been played.
// Increases by 1 with each ChooseTrackServerRpc call.
// Resets on SettingChanged.
private int SelectedTrackIndex = 0;
public override void OnNetworkSpawn() public override void OnNetworkSpawn()
{ {
ChooseTrackDeferred(); ChooseTrackDeferred();
@ -2268,7 +2042,6 @@ namespace MuzikaGromche
private void ChooseTrackDeferredDelegate(object sender, EventArgs e) private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
{ {
SelectedTrackIndex = 0;
ChooseTrackDeferred(); ChooseTrackDeferred();
} }
@ -2299,15 +2072,13 @@ namespace MuzikaGromche
[ServerRpc] [ServerRpc]
public void ChooseTrackServerRpc() public void ChooseTrackServerRpc()
{ {
var selectableTrack = Plugin.ChooseTrack(); var track = Plugin.ChooseTrack();
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex); Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {track.Name}");
Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}"); SetTrackClientRpc(track.Name);
SetTrackClientRpc(audioTrack.Name);
SelectedTrackIndex += 1;
} }
} }
// farAudio is during windup, Intro overrides popGoesTheWeaselTheme // farAudio is during windup, Start overrides popGoesTheWeaselTheme
// creatureVoice is when popped, Loop overrides screamingSFX // creatureVoice is when popped, Loop overrides screamingSFX
[HarmonyPatch(typeof(JesterAI))] [HarmonyPatch(typeof(JesterAI))]
static class JesterPatch static class JesterPatch
@ -2338,7 +2109,7 @@ namespace MuzikaGromche
}; };
if (__instance.currentBehaviourStateIndex == 2 && __instance.previousState != 2) if (__instance.currentBehaviourStateIndex == 2 && __instance.previousState != 2)
{ {
// If just popped out, then override farAudio so that vanilla logic does not stop the modded Intro music. // If just popped out, then override farAudio so that vanilla logic does not stop the modded Start music.
// The game will stop farAudio it during its Update, so we temporarily set it to any other AudioSource // The game will stop farAudio it during its Update, so we temporarily set it to any other AudioSource
// which we don't care about stopping for now. // which we don't care about stopping for now.
// //
@ -2375,7 +2146,7 @@ namespace MuzikaGromche
// Override popGoesTheWeaselTheme with Start audio // Override popGoesTheWeaselTheme with Start audio
__instance.farAudio.maxDistance = Plugin.AudioMaxDistance; __instance.farAudio.maxDistance = Plugin.AudioMaxDistance;
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro; __instance.farAudio.clip = Plugin.CurrentTrack.LoadedStart;
__instance.farAudio.loop = false; __instance.farAudio.loop = false;
if (Config.ShouldSkipWindingPhase) if (Config.ShouldSkipWindingPhase)
{ {
@ -2390,15 +2161,13 @@ namespace MuzikaGromche
} }
__instance.farAudio.Play(); __instance.farAudio.Play();
Debug.Log($"{nameof(MuzikaGromche)} Playing Intro music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}"); Debug.Log($"{nameof(MuzikaGromche)} Playing start music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}");
} }
if (__instance.previousState != 2 && __state.previousState == 2) if (__instance.previousState != 2 && __state.previousState == 2)
{ {
Plugin.ResetLightColor(); Plugin.ResetLightColor();
DiscoBallManager.Disable(); DiscoBallManager.Disable();
// Rotate track groups
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc();
} }
if (__instance.previousState == 2 && __state.previousState != 2) if (__instance.previousState == 2 && __state.previousState != 2)
@ -2407,22 +2176,22 @@ namespace MuzikaGromche
__instance.farAudio = __state.farAudio; __instance.farAudio = __state.farAudio;
var time = __instance.farAudio.time; var time = __instance.farAudio.time;
var delay = Plugin.CurrentTrack.LoadedIntro.length - time; var delay = Plugin.CurrentTrack.LoadedStart.length - time;
// Override screamingSFX with Loop, delayed by the remaining time of the Intro audio // Override screamingSFX with Loop, delayed by the remaining time of the Start audio
__instance.creatureVoice.Stop(); __instance.creatureVoice.Stop();
__instance.creatureVoice.maxDistance = Plugin.AudioMaxDistance; __instance.creatureVoice.maxDistance = Plugin.AudioMaxDistance;
__instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop; __instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop;
__instance.creatureVoice.PlayDelayed(delay); __instance.creatureVoice.PlayDelayed(delay);
Debug.Log($"{nameof(MuzikaGromche)} Intro length: {Plugin.CurrentTrack.LoadedIntro.length}; played time: {time}"); Debug.Log($"{nameof(MuzikaGromche)} Start length: {Plugin.CurrentTrack.LoadedStart.length}; played time: {time}");
Debug.Log($"{nameof(MuzikaGromche)} Playing loop music: maxDistance: {__instance.creatureVoice.maxDistance}, minDistance: {__instance.creatureVoice.minDistance}, volume: {__instance.creatureVoice.volume}, spread: {__instance.creatureVoice.spread}, in seconds: {delay}"); Debug.Log($"{nameof(MuzikaGromche)} Playing loop music: maxDistance: {__instance.creatureVoice.maxDistance}, minDistance: {__instance.creatureVoice.minDistance}, volume: {__instance.creatureVoice.volume}, spread: {__instance.creatureVoice.spread}, in seconds: {delay}");
} }
// Manage the timeline: switch color of the lights according to the current playback/beat position. // Manage the timeline: switch color of the lights according to the current playback/beat position.
if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState is { } beatTimeState) if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState is { } beatTimeState)
{ {
var events = beatTimeState.Update(intro: __instance.farAudio, loop: __instance.creatureVoice); var events = beatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
foreach (var ev in events) foreach (var ev in events)
{ {
switch (ev) switch (ev)

View File

@ -1,6 +1,6 @@
{ {
"name": "MuzikaGromche", "name": "MuzikaGromche",
"version_number": "1337.420.9001", "version_number": "1337.420.69",
"author": "Ratijas", "author": "Ratijas",
"description": "Add some content to your inverse teleporter experience on Titan!", "description": "Add some content to your inverse teleporter experience on Titan!",
"website_url": "https://git.vilunov.me/ratijas/muzika-gromche", "website_url": "https://git.vilunov.me/ratijas/muzika-gromche",