forked from nikita/muzika-gromche
				
			Compare commits
	
		
			7 Commits
		
	
	
		
			df796965f2
			...
			6a9ea8d4af
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 6a9ea8d4af | |
|  | 42c6179ba5 | |
|  | 5649a18633 | |
|  | 47f984cd28 | |
|  | fc3a62e511 | |
|  | 5f0c890682 | |
|  | 59a069f51b | 
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,10 +1,15 @@ | ||||||
| # 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 | ||||||
| 
 | 
 | ||||||
| - Fix harmless but annoying errors in BepInEx console output. | - Fixed harmless but annoying errors in BepInEx console output. | ||||||
| - Improve smoothness of color animations. | - Improve smoothness of color animations. | ||||||
| - Add a new track. | - Added a new track BeefLiver. | ||||||
| 
 | 
 | ||||||
| ## MuzikaGromche 1337.69.420 - It's All Connected Edition | ## MuzikaGromche 1337.69.420 - It's All Connected Edition | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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.69</Version> |         <Version>1337.420.9001</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)Start" /> |             <TrackNames Include="$(TrackName)Intro" /> | ||||||
|             <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" /> | ||||||
|  |  | ||||||
|  | @ -48,8 +48,8 @@ namespace MuzikaGromche | ||||||
|                 .Select(a => $" Trying... {a}") |                 .Select(a => $" Trying... {a}") | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         public static readonly Track[] Tracks = [ |         public static readonly ISelectableTrack[] Tracks = [ | ||||||
|             new Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 Name = "DeployDestroy", |                 Name = "DeployDestroy", | ||||||
|                 AudioType = AudioType.OGGVORBIS, |                 AudioType = AudioType.OGGVORBIS, | ||||||
|  | @ -167,7 +167,7 @@ namespace MuzikaGromche | ||||||
|                     (25, "Davaj-davaj!"), |                     (25, "Davaj-davaj!"), | ||||||
|                 ], |                 ], | ||||||
|             }, |             }, | ||||||
|             new Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 Name = "Gorgorod", |                 Name = "Gorgorod", | ||||||
|                 AudioType = AudioType.OGGVORBIS, |                 AudioType = AudioType.OGGVORBIS, | ||||||
|  | @ -232,7 +232,7 @@ namespace MuzikaGromche | ||||||
|                 FlickerLightsTimeSeries = [20], |                 FlickerLightsTimeSeries = [20], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|             }, |             }, | ||||||
|             new Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 Name = "Durochka", |                 Name = "Durochka", | ||||||
|                 AudioType = AudioType.OGGVORBIS, |                 AudioType = AudioType.OGGVORBIS, | ||||||
|  | @ -250,7 +250,7 @@ namespace MuzikaGromche | ||||||
|                 FlickerLightsTimeSeries = [-9], |                 FlickerLightsTimeSeries = [-9], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|             }, |             }, | ||||||
|             new Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 Name = "GodMode", |                 Name = "GodMode", | ||||||
|                 AudioType = AudioType.OGGVORBIS, |                 AudioType = AudioType.OGGVORBIS, | ||||||
|  | @ -286,7 +286,7 @@ namespace MuzikaGromche | ||||||
|                 FlickerLightsTimeSeries = [-5], |                 FlickerLightsTimeSeries = [-5], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|             }, |             }, | ||||||
|             new Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 Name = "Yalgaar", |                 Name = "Yalgaar", | ||||||
|                 AudioType = AudioType.OGGVORBIS, |                 AudioType = AudioType.OGGVORBIS, | ||||||
|  | @ -358,7 +358,7 @@ namespace MuzikaGromche | ||||||
|                 FlickerLightsTimeSeries = [-5], |                 FlickerLightsTimeSeries = [-5], | ||||||
|                 Lyrics = [], |                 Lyrics = [], | ||||||
|             }, |             }, | ||||||
|             new Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 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 Track |             new SelectableAudioTrack | ||||||
|             { |             { | ||||||
|                 Name = "BeefLiver", |                 Name = "BeefLiver", | ||||||
|                 AudioType = AudioType.OGGVORBIS, |                 AudioType = AudioType.OGGVORBIS, | ||||||
|  | @ -496,9 +496,78 @@ 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 Track ChooseTrack() |         public static ISelectableTrack 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; | ||||||
|  | @ -510,12 +579,12 @@ namespace MuzikaGromche | ||||||
|             return tracks[trackId]; |             return tracks[trackId]; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static Track? FindTrackNamed(string name) |         public static IAudioTrack? FindTrackNamed(string name) | ||||||
|         { |         { | ||||||
|             return Tracks.FirstOrDefault(track => track.Name == name); |             return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         internal static Track? CurrentTrack; |         internal static IAudioTrack? CurrentTrack; | ||||||
|         internal static BeatTimeState? BeatTimeState; |         internal static BeatTimeState? BeatTimeState; | ||||||
| 
 | 
 | ||||||
|         public static void SetLightColor(Color color) |         public static void SetLightColor(Color color) | ||||||
|  | @ -559,29 +628,49 @@ 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); | ||||||
|             UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; |             Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> requests = []; | ||||||
|             for (int i = 0; i < Tracks.Length; i++) |             requests.EnsureCapacity(Tracks.Length * 2); | ||||||
|  | 
 | ||||||
|  |             foreach (var track in Tracks.SelectMany(track => track.GetTracks())) | ||||||
|             { |             { | ||||||
|                 Track track = Tracks[i]; |                 foreach (var (fileName, setter) in new (string, Action<AudioClip>)[] | ||||||
|                 requests[i * 2] = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{track.FileNameStart}", track.AudioType); |                 { | ||||||
|                 requests[i * 2 + 1] = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{track.FileNameLoop}", track.AudioType); |                     (track.FileNameIntro, clip => track.LoadedIntro = clip), | ||||||
|                 requests[i * 2].SendWebRequest(); |                     (track.FileNameLoop, clip => track.LoadedLoop = clip), | ||||||
|                 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.All(request => request.isDone)) { } |             while (!requests.Values.All(tuple => tuple.Request.isDone)) { } | ||||||
| 
 | 
 | ||||||
|             if (requests.All(request => request.result == UnityWebRequest.Result.Success)) |             if (requests.Values.All(tuple => tuple.Request.result == UnityWebRequest.Result.Success)) | ||||||
|             { |             { | ||||||
|                 for (int i = 0; i < Tracks.Length; i++) | 
 | ||||||
|  |                 foreach (var (fileName, tuple) in requests) | ||||||
|                 { |                 { | ||||||
|                     Track track = Tracks[i]; |                     var clip = DownloadHandlerAudioClip.GetContent(tuple.Request); | ||||||
|                     track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]); |                     foreach (var setter in tuple.Setters) | ||||||
|                     track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); |                     { | ||||||
| #if DEBUG |                         setter(clip); | ||||||
|                     Debug.Log($"{nameof(MuzikaGromche)} Track {track.Name} {track.LoadedStart.length:N4} {track.LoadedLoop.length:N4}"); |                     } | ||||||
| #endif |  | ||||||
|                 } |                 } | ||||||
|  | #if DEBUG | ||||||
|  |                 foreach (var track in Tracks) | ||||||
|  |                 { | ||||||
|  |                     track.Debug(); | ||||||
|  |                 } | ||||||
|  | #endif | ||||||
|                 Config = new Config(base.Config); |                 Config = new Config(base.Config); | ||||||
|                 DiscoBallManager.Load(); |                 DiscoBallManager.Load(); | ||||||
|                 PoweredLightsAnimators.Load(); |                 PoweredLightsAnimators.Load(); | ||||||
|  | @ -598,7 +687,7 @@ namespace MuzikaGromche | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 var failed = requests.Where(request => request.result != UnityWebRequest.Result.Success).Select(request => request.GetUrl()); |                 var failed = requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl()); | ||||||
|                 Logger.LogError("Could not load audio file " + string.Join(", ", failed)); |                 Logger.LogError("Could not load audio file " + string.Join(", ", failed)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -701,48 +790,80 @@ namespace MuzikaGromche | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public class Track |     public struct SelectableTrackData() | ||||||
|     { |     { | ||||||
|         public required string Name; |         // Name of the track, as shown in config entry UI; also used for default file names. | ||||||
|  |         public required string Name { get; init; } | ||||||
|  | 
 | ||||||
|         // Language of the track's lyrics. |         // Language of the track's lyrics. | ||||||
|         public required Language Language; |         public required Language Language { get; init; } | ||||||
|  | 
 | ||||||
|         // Whether this track has NSFW/explicit lyrics. |         // Whether this track has NSFW/explicit lyrics. | ||||||
|         public bool IsExplicit = false; |         public bool IsExplicit { get; init; } = false; | ||||||
|         // Wind-up time can and should be shorter than the Start audio track, | 
 | ||||||
|         // so that the "pop" effect can be baked into the Start audio and kept away |         // How often this track should be chosen, relative to the sum of weights of all tracks. | ||||||
|  |         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 required float WindUpTimer; |         public float WindUpTimer { get; } | ||||||
| 
 | 
 | ||||||
|         // 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; |         public int Beats { get; } | ||||||
| 
 | 
 | ||||||
|         // 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 = 0; |         public int LoopOffset { get; } | ||||||
|         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 = AudioType.MPEG; |         public AudioType AudioType { get; } | ||||||
| 
 | 
 | ||||||
|         public AudioClip LoadedStart = null!; |         public AudioClip LoadedIntro { get; internal set; } | ||||||
|         public AudioClip LoadedLoop = null!; |         public AudioClip LoadedLoop { get; internal set; } | ||||||
| 
 | 
 | ||||||
|         // How often this track should be chosen, relative to the sum of weights of all tracks. |         public string FileNameIntro { get; } | ||||||
|         public ConfigEntry<int> Weight = null!; |         public string FileNameLoop { get; } | ||||||
| 
 | 
 | ||||||
|         public string FileNameStart => $"{Name}Start.{Ext}"; |         public string Ext => AudioType switch | ||||||
|         public string FileNameLoop => $"{Name}Loop.{Ext}"; |  | ||||||
|         private string Ext => AudioType switch |  | ||||||
|         { |         { | ||||||
|             AudioType.MPEG => "mp3", |             AudioType.MPEG => "mp3", | ||||||
|             AudioType.WAV => "wav", |             AudioType.WAV => "wav", | ||||||
|  | @ -751,28 +872,86 @@ 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; | ||||||
|             set => _BeatsOffset = value; |             init => _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; | ||||||
|             set => _FadeOutBeat = value; |             init => _FadeOutBeat = value; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public float _FadeOutDuration = 2f; |         public float _FadeOutDuration = 2f; | ||||||
|         public float FadeOutDuration |         public float FadeOutDuration | ||||||
|         { |         { | ||||||
|             get => Config.FadeOutDurationOverride ?? _FadeOutDuration; |             get => Config.FadeOutDurationOverride ?? _FadeOutDuration; | ||||||
|             set => _FadeOutDuration = value; |             init => _FadeOutDuration = value; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Duration of color transition, measured in beats. |         // Duration of color transition, measured in beats. | ||||||
|  | @ -780,14 +959,14 @@ namespace MuzikaGromche | ||||||
|         public float ColorTransitionIn |         public float ColorTransitionIn | ||||||
|         { |         { | ||||||
|             get => Config.ColorTransitionInOverride ?? _ColorTransitionIn; |             get => Config.ColorTransitionInOverride ?? _ColorTransitionIn; | ||||||
|             set => _ColorTransitionIn = value; |             init => _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; | ||||||
|             set => _ColorTransitionOut = value; |             init => _ColorTransitionOut = value; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Easing function for color transitions. |         // Easing function for color transitions. | ||||||
|  | @ -797,14 +976,14 @@ namespace MuzikaGromche | ||||||
|             get => Config.ColorTransitionEasingOverride != null |             get => Config.ColorTransitionEasingOverride != null | ||||||
|                 ? Easing.FindByName(Config.ColorTransitionEasingOverride) |                 ? Easing.FindByName(Config.ColorTransitionEasingOverride) | ||||||
|                 : _ColorTransitionEasing; |                 : _ColorTransitionEasing; | ||||||
|             set => _ColorTransitionEasing = value; |             init => _ColorTransitionEasing = value; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public float[] _FlickerLightsTimeSeries = []; |         public float[] _FlickerLightsTimeSeries = []; | ||||||
|         public float[] FlickerLightsTimeSeries |         public float[] FlickerLightsTimeSeries | ||||||
|         { |         { | ||||||
|             get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries; |             get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries; | ||||||
|             set |             init | ||||||
|             { |             { | ||||||
|                 Array.Sort(value); |                 Array.Sort(value); | ||||||
|                 _FlickerLightsTimeSeries = value; |                 _FlickerLightsTimeSeries = value; | ||||||
|  | @ -845,6 +1024,53 @@ 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. | ||||||
|  | @ -1099,11 +1325,6 @@ 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')} " | ||||||
|  | @ -1116,36 +1337,36 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|     class JesterAudioSourcesState |     class JesterAudioSourcesState | ||||||
|     { |     { | ||||||
|         private readonly float StartClipLength; |         private readonly float IntroClipLength; | ||||||
| 
 | 
 | ||||||
|         // Neither start.isPlaying or loop.isPlaying are reliable indicators of which track is actually playing right now: |         // Neither intro.isPlaying or loop.isPlaying are reliable indicators of which track is actually playing right now: | ||||||
|         // start.isPlaying would be true during the loop when Jester chases a player, |         // intro.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 Start = new(); |         private readonly ExtrapolatedAudioSourceState Intro = 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 ReferenceIsStart = true; |         private bool ReferenceIsIntro = true; | ||||||
| 
 | 
 | ||||||
|         public bool HasStarted => Start.HasStarted; |         public bool HasStarted => Intro.HasStarted; | ||||||
| 
 | 
 | ||||||
|         public bool IsExtrapolated => ReferenceIsStart ? Start.IsExtrapolated : Loop.IsExtrapolated; |         public bool IsExtrapolated => ReferenceIsIntro ? Intro.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 => ReferenceIsStart |         public float Time => ReferenceIsIntro | ||||||
|             ? Start.Time |             ? Intro.Time | ||||||
|             : StartClipLength + Loop.Time; |             : IntroClipLength + Loop.Time; | ||||||
| 
 | 
 | ||||||
|         public JesterAudioSourcesState(float startClipLength) |         public JesterAudioSourcesState(float introClipLength) | ||||||
|         { |         { | ||||||
|             StartClipLength = startClipLength; |             IntroClipLength = introClipLength; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public void Update(AudioSource start, AudioSource loop, float realtime) |         public void Update(AudioSource intro, 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. | ||||||
|  | @ -1154,13 +1375,13 @@ namespace MuzikaGromche | ||||||
|             if (!Loop.HasStarted) |             if (!Loop.HasStarted) | ||||||
|             { |             { | ||||||
| #if DEBUG | #if DEBUG | ||||||
|                 Debug.Assert(ReferenceIsStart); |                 Debug.Assert(ReferenceIsIntro); | ||||||
| #endif | #endif | ||||||
|                 Start.Update(start, realtime); |                 Intro.Update(intro, realtime); | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 ReferenceIsStart = false; |                 ReferenceIsIntro = false; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -1196,7 +1417,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: | ||||||
|             //         Start/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that. |             //         Intro/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that. | ||||||
| 
 | 
 | ||||||
|             var offset = StartOfLoop + additionalOffset; |             var offset = StartOfLoop + additionalOffset; | ||||||
| 
 | 
 | ||||||
|  | @ -1225,7 +1446,7 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|     class BeatTimeState |     class BeatTimeState | ||||||
|     { |     { | ||||||
|         private readonly Track track; |         private readonly IAudioTrack track; | ||||||
| 
 | 
 | ||||||
|         private readonly JesterAudioSourcesState AudioState; |         private readonly JesterAudioSourcesState AudioState; | ||||||
| 
 | 
 | ||||||
|  | @ -1243,7 +1464,7 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         private bool WindUpZeroBeatEventTriggered = false; |         private bool WindUpZeroBeatEventTriggered = false; | ||||||
| 
 | 
 | ||||||
|         public BeatTimeState(Track track) |         public BeatTimeState(IAudioTrack track) | ||||||
|         { |         { | ||||||
|             if (LyricsRandom == null) |             if (LyricsRandom == null) | ||||||
|             { |             { | ||||||
|  | @ -1251,15 +1472,15 @@ namespace MuzikaGromche | ||||||
|                 LyricsRandomPerLoop = LyricsRandom.Next(); |                 LyricsRandomPerLoop = LyricsRandom.Next(); | ||||||
|             } |             } | ||||||
|             this.track = track; |             this.track = track; | ||||||
|             AudioState = new(track.LoadedStart.length); |             AudioState = new(track.LoadedIntro.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 start, AudioSource loop) |         public List<BaseEvent> Update(AudioSource intro, AudioSource loop) | ||||||
|         { |         { | ||||||
|             var time = Time.realtimeSinceStartup; |             var time = Time.realtimeSinceStartup; | ||||||
|             AudioState.Update(start, loop, time); |             AudioState.Update(intro, loop, time); | ||||||
| 
 | 
 | ||||||
|             if (AudioState.HasStarted) |             if (AudioState.HasStarted) | ||||||
|             { |             { | ||||||
|  | @ -1777,7 +1998,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/start music.\n\nUse this option to test your Loop audio segment.")); |                 new ConfigDescription("Skip most of the wind-up/intro 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(); | ||||||
|  | @ -1826,7 +2047,7 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|             void load() |             void load() | ||||||
|             { |             { | ||||||
|                 var palette = Plugin.CurrentTrack?._Palette ?? Palette.DEFAULT; |                 var palette = (Plugin.CurrentTrack as CoreAudioTrack)?._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); | ||||||
| 
 | 
 | ||||||
|  | @ -1859,7 +2080,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<Track?> Load, Action Apply)> entries = []; |             List<(Action<CoreAudioTrack?> 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!; | ||||||
|  | @ -1918,12 +2139,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<Track, T> getter, Action applier) |             void register<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action applier) | ||||||
|             { |             { | ||||||
|                 CSyncHackAddSyncedEntry(syncedEntry); |                 CSyncHackAddSyncedEntry(syncedEntry); | ||||||
|                 syncedEntry.SyncHostToLocal(); |                 syncedEntry.SyncHostToLocal(); | ||||||
|                 syncedEntry.Changed += (sender, args) => applier(); |                 syncedEntry.Changed += (sender, args) => applier(); | ||||||
|                 void loader(Track? track) |                 void loader(CoreAudioTrack? 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); | ||||||
|  | @ -1931,11 +2152,11 @@ namespace MuzikaGromche | ||||||
|                 entries.Add((loader, applier)); |                 entries.Add((loader, applier)); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<Track, T> getter, Action<T?> setter) where T : struct => |             void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, 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<Track, T> getter, Action<T?> setter) where T : class => |             void registerClass<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, 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<Track, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct => |             void registerArray<T>(SyncedEntry<string> syncedEntry, Func<CoreAudioTrack, 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)), | ||||||
|                     () => |                     () => | ||||||
|  | @ -1969,7 +2190,7 @@ namespace MuzikaGromche | ||||||
|                 var track = Plugin.CurrentTrack; |                 var track = Plugin.CurrentTrack; | ||||||
|                 foreach (var entry in entries) |                 foreach (var entry in entries) | ||||||
|                 { |                 { | ||||||
|                     entry.Load(track); |                     entry.Load(track as CoreAudioTrack); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -2016,6 +2237,11 @@ 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(); | ||||||
|  | @ -2042,6 +2268,7 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         private void ChooseTrackDeferredDelegate(object sender, EventArgs e) |         private void ChooseTrackDeferredDelegate(object sender, EventArgs e) | ||||||
|         { |         { | ||||||
|  |             SelectedTrackIndex = 0; | ||||||
|             ChooseTrackDeferred(); |             ChooseTrackDeferred(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -2072,13 +2299,15 @@ namespace MuzikaGromche | ||||||
|         [ServerRpc] |         [ServerRpc] | ||||||
|         public void ChooseTrackServerRpc() |         public void ChooseTrackServerRpc() | ||||||
|         { |         { | ||||||
|             var track = Plugin.ChooseTrack(); |             var selectableTrack = Plugin.ChooseTrack(); | ||||||
|             Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {track.Name}"); |             var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex); | ||||||
|             SetTrackClientRpc(track.Name); |             Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}"); | ||||||
|  |             SetTrackClientRpc(audioTrack.Name); | ||||||
|  |             SelectedTrackIndex += 1; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // farAudio is during windup, Start overrides popGoesTheWeaselTheme |     // farAudio is during windup, Intro 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 | ||||||
|  | @ -2109,7 +2338,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 Start music. |                 // If just popped out, then override farAudio so that vanilla logic does not stop the modded Intro 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. | ||||||
|                 // |                 // | ||||||
|  | @ -2146,7 +2375,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.LoadedStart; |                 __instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro; | ||||||
|                 __instance.farAudio.loop = false; |                 __instance.farAudio.loop = false; | ||||||
|                 if (Config.ShouldSkipWindingPhase) |                 if (Config.ShouldSkipWindingPhase) | ||||||
|                 { |                 { | ||||||
|  | @ -2161,13 +2390,15 @@ namespace MuzikaGromche | ||||||
|                 } |                 } | ||||||
|                 __instance.farAudio.Play(); |                 __instance.farAudio.Play(); | ||||||
| 
 | 
 | ||||||
|                 Debug.Log($"{nameof(MuzikaGromche)} Playing start music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}"); |                 Debug.Log($"{nameof(MuzikaGromche)} Playing Intro 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) | ||||||
|  | @ -2176,22 +2407,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.LoadedStart.length - time; |                 var delay = Plugin.CurrentTrack.LoadedIntro.length - time; | ||||||
| 
 | 
 | ||||||
|                 // Override screamingSFX with Loop, delayed by the remaining time of the Start audio |                 // Override screamingSFX with Loop, delayed by the remaining time of the Intro 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)} Start length: {Plugin.CurrentTrack.LoadedStart.length}; played time: {time}"); |                 Debug.Log($"{nameof(MuzikaGromche)} Intro length: {Plugin.CurrentTrack.LoadedIntro.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(start: __instance.farAudio, loop: __instance.creatureVoice); |                 var events = beatTimeState.Update(intro: __instance.farAudio, loop: __instance.creatureVoice); | ||||||
|                 foreach (var ev in events) |                 foreach (var ev in events) | ||||||
|                 { |                 { | ||||||
|                     switch (ev) |                     switch (ev) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|     "name": "MuzikaGromche", |     "name": "MuzikaGromche", | ||||||
|     "version_number": "1337.420.69", |     "version_number": "1337.420.9001", | ||||||
|     "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", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue