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
|
||||
|
||||
## 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
|
||||
|
||||
- Fix harmless but annoying errors in BepInEx console output.
|
||||
- Fixed harmless but annoying errors in BepInEx console output.
|
||||
- Improve smoothness of color animations.
|
||||
- Add a new track.
|
||||
- Added a new track BeefLiver.
|
||||
|
||||
## MuzikaGromche 1337.69.420 - It's All Connected Edition
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<AssemblyName>Ratijas.MuzikaGromche</AssemblyName>
|
||||
<Product>Muzika Gromche</Product>
|
||||
<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>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
@ -110,7 +110,7 @@
|
|||
-->
|
||||
<Target Name="wav2ogg">
|
||||
<ItemGroup>
|
||||
<TrackNames Include="$(TrackName)Start" />
|
||||
<TrackNames Include="$(TrackName)Intro" />
|
||||
<TrackNames Include="$(TrackName)Loop" />
|
||||
</ItemGroup>
|
||||
<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}")
|
||||
];
|
||||
|
||||
public static readonly Track[] Tracks = [
|
||||
new Track
|
||||
public static readonly ISelectableTrack[] Tracks = [
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "MuzikaGromche",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -89,7 +89,7 @@ namespace MuzikaGromche
|
|||
(63, "Muzyka Gromche\nGlaza zakryty >_<"),
|
||||
],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "VseVZale",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -124,7 +124,7 @@ namespace MuzikaGromche
|
|||
(60, "Everybody shake your body"),
|
||||
],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "DeployDestroy",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -167,7 +167,7 @@ namespace MuzikaGromche
|
|||
(25, "Davaj-davaj!"),
|
||||
],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "MoyaZhittya",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -214,7 +214,7 @@ namespace MuzikaGromche
|
|||
( 30, "IT'S MY"),
|
||||
],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Gorgorod",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -232,7 +232,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [20],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Durochka",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -250,7 +250,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-9],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "ZmeiGorynich",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -268,7 +268,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-5, 31],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "GodMode",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -286,7 +286,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-5],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "RiseAndShine",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -304,7 +304,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-5.5f, 31, 63.9f],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Song2",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -322,7 +322,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [2.5f],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Peretasovka",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -340,7 +340,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-8, 31],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Yalgaar",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -358,7 +358,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-5],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Chereshnya",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -379,7 +379,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-5, 27, 29, 59, 61],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "PWNED",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -451,7 +451,7 @@ namespace MuzikaGromche
|
|||
(98, $"\t\t\tresolving ur private IP\nP_WNED"),
|
||||
],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "Kach",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -475,7 +475,7 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45],
|
||||
Lyrics = [],
|
||||
},
|
||||
new Track
|
||||
new SelectableAudioTrack
|
||||
{
|
||||
Name = "BeefLiver",
|
||||
AudioType = AudioType.OGGVORBIS,
|
||||
|
@ -496,9 +496,78 @@ namespace MuzikaGromche
|
|||
FlickerLightsTimeSeries = [-48, -40, -4.5f, 44],
|
||||
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 tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
|
||||
|
@ -510,12 +579,12 @@ namespace MuzikaGromche
|
|||
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;
|
||||
|
||||
public static void SetLightColor(Color color)
|
||||
|
@ -559,29 +628,49 @@ namespace MuzikaGromche
|
|||
Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks);
|
||||
|
||||
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2];
|
||||
for (int i = 0; i < Tracks.Length; i++)
|
||||
Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> requests = [];
|
||||
requests.EnsureCapacity(Tracks.Length * 2);
|
||||
|
||||
foreach (var track in Tracks.SelectMany(track => track.GetTracks()))
|
||||
{
|
||||
Track track = Tracks[i];
|
||||
requests[i * 2] = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{track.FileNameStart}", track.AudioType);
|
||||
requests[i * 2 + 1] = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{track.FileNameLoop}", track.AudioType);
|
||||
requests[i * 2].SendWebRequest();
|
||||
requests[i * 2 + 1].SendWebRequest();
|
||||
foreach (var (fileName, setter) in new (string, Action<AudioClip>)[]
|
||||
{
|
||||
(track.FileNameIntro, clip => track.LoadedIntro = clip),
|
||||
(track.FileNameLoop, clip => track.LoadedLoop = clip),
|
||||
})
|
||||
{
|
||||
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];
|
||||
track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]);
|
||||
track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]);
|
||||
var clip = DownloadHandlerAudioClip.GetContent(tuple.Request);
|
||||
foreach (var setter in tuple.Setters)
|
||||
{
|
||||
setter(clip);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
Debug.Log($"{nameof(MuzikaGromche)} Track {track.Name} {track.LoadedStart.length:N4} {track.LoadedLoop.length:N4}");
|
||||
#endif
|
||||
foreach (var track in Tracks)
|
||||
{
|
||||
track.Debug();
|
||||
}
|
||||
#endif
|
||||
Config = new Config(base.Config);
|
||||
DiscoBallManager.Load();
|
||||
PoweredLightsAnimators.Load();
|
||||
|
@ -598,7 +687,7 @@ namespace MuzikaGromche
|
|||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
public required Language Language;
|
||||
public required Language Language { get; init; }
|
||||
|
||||
// Whether this track has NSFW/explicit lyrics.
|
||||
public bool IsExplicit = 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
|
||||
public bool IsExplicit { get; init; } = false;
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
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.
|
||||
public int Beats;
|
||||
public int Beats { get; }
|
||||
|
||||
// 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;
|
||||
|
||||
// Shorthand for four beats
|
||||
public int Bars
|
||||
{
|
||||
set => Beats = value * 4;
|
||||
}
|
||||
|
||||
// MPEG is basically mp3, and it can produce gaps at the start.
|
||||
// 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 LoadedLoop = null!;
|
||||
public AudioClip LoadedIntro { get; internal set; }
|
||||
public AudioClip LoadedLoop { get; internal set; }
|
||||
|
||||
// How often this track should be chosen, relative to the sum of weights of all tracks.
|
||||
public ConfigEntry<int> Weight = null!;
|
||||
public string FileNameIntro { get; }
|
||||
public string FileNameLoop { get; }
|
||||
|
||||
public string FileNameStart => $"{Name}Start.{Ext}";
|
||||
public string FileNameLoop => $"{Name}Loop.{Ext}";
|
||||
private string Ext => AudioType switch
|
||||
public string Ext => AudioType switch
|
||||
{
|
||||
AudioType.MPEG => "mp3",
|
||||
AudioType.WAV => "wav",
|
||||
|
@ -751,28 +872,86 @@ namespace MuzikaGromche
|
|||
};
|
||||
|
||||
// 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
|
||||
{
|
||||
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
|
||||
{
|
||||
get => Config.FadeOutBeatOverride ?? _FadeOutBeat;
|
||||
set => _FadeOutBeat = value;
|
||||
init => _FadeOutBeat = value;
|
||||
}
|
||||
|
||||
public float _FadeOutDuration = 2f;
|
||||
public float FadeOutDuration
|
||||
{
|
||||
get => Config.FadeOutDurationOverride ?? _FadeOutDuration;
|
||||
set => _FadeOutDuration = value;
|
||||
init => _FadeOutDuration = value;
|
||||
}
|
||||
|
||||
// Duration of color transition, measured in beats.
|
||||
|
@ -780,14 +959,14 @@ namespace MuzikaGromche
|
|||
public float ColorTransitionIn
|
||||
{
|
||||
get => Config.ColorTransitionInOverride ?? _ColorTransitionIn;
|
||||
set => _ColorTransitionIn = value;
|
||||
init => _ColorTransitionIn = value;
|
||||
}
|
||||
|
||||
public float _ColorTransitionOut = 0.25f;
|
||||
public float ColorTransitionOut
|
||||
{
|
||||
get => Config.ColorTransitionOutOverride ?? _ColorTransitionOut;
|
||||
set => _ColorTransitionOut = value;
|
||||
init => _ColorTransitionOut = value;
|
||||
}
|
||||
|
||||
// Easing function for color transitions.
|
||||
|
@ -797,14 +976,14 @@ namespace MuzikaGromche
|
|||
get => Config.ColorTransitionEasingOverride != null
|
||||
? Easing.FindByName(Config.ColorTransitionEasingOverride)
|
||||
: _ColorTransitionEasing;
|
||||
set => _ColorTransitionEasing = value;
|
||||
init => _ColorTransitionEasing = value;
|
||||
}
|
||||
|
||||
public float[] _FlickerLightsTimeSeries = [];
|
||||
public float[] FlickerLightsTimeSeries
|
||||
{
|
||||
get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries;
|
||||
set
|
||||
init
|
||||
{
|
||||
Array.Sort(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
|
||||
{
|
||||
// Number of beats in the loop audio segment.
|
||||
|
@ -1099,11 +1325,6 @@ namespace MuzikaGromche
|
|||
}
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
IsPlaying = false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} "
|
||||
|
@ -1116,36 +1337,36 @@ namespace MuzikaGromche
|
|||
|
||||
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:
|
||||
// start.isPlaying would be true during the loop when Jester chases a player,
|
||||
// Neither intro.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,
|
||||
// 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();
|
||||
|
||||
// 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:
|
||||
// [...start...][...loop...]
|
||||
// ^ |
|
||||
// `----------'
|
||||
public float Time => ReferenceIsStart
|
||||
? Start.Time
|
||||
: StartClipLength + Loop.Time;
|
||||
public float Time => ReferenceIsIntro
|
||||
? Intro.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).
|
||||
// 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 DEBUG
|
||||
Debug.Assert(ReferenceIsStart);
|
||||
Debug.Assert(ReferenceIsIntro);
|
||||
#endif
|
||||
Start.Update(start, realtime);
|
||||
Intro.Update(intro, realtime);
|
||||
}
|
||||
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 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;
|
||||
|
||||
|
@ -1225,7 +1446,7 @@ namespace MuzikaGromche
|
|||
|
||||
class BeatTimeState
|
||||
{
|
||||
private readonly Track track;
|
||||
private readonly IAudioTrack track;
|
||||
|
||||
private readonly JesterAudioSourcesState AudioState;
|
||||
|
||||
|
@ -1243,7 +1464,7 @@ namespace MuzikaGromche
|
|||
|
||||
private bool WindUpZeroBeatEventTriggered = false;
|
||||
|
||||
public BeatTimeState(Track track)
|
||||
public BeatTimeState(IAudioTrack track)
|
||||
{
|
||||
if (LyricsRandom == null)
|
||||
{
|
||||
|
@ -1251,15 +1472,15 @@ namespace MuzikaGromche
|
|||
LyricsRandomPerLoop = LyricsRandom.Next();
|
||||
}
|
||||
this.track = track;
|
||||
AudioState = new(track.LoadedStart.length);
|
||||
AudioState = new(track.LoadedIntro.length);
|
||||
WindUpLoopingState = new(track.WindUpTimer, 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;
|
||||
AudioState.Update(start, loop, time);
|
||||
AudioState.Update(intro, loop, time);
|
||||
|
||||
if (AudioState.HasStarted)
|
||||
{
|
||||
|
@ -1777,7 +1998,7 @@ namespace MuzikaGromche
|
|||
private void SetupEntriesToSkipWinding(ConfigFile configFile)
|
||||
{
|
||||
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())));
|
||||
CSyncHackAddSyncedEntry(syncedEntry);
|
||||
syncedEntry.Changed += (sender, args) => apply();
|
||||
|
@ -1826,7 +2047,7 @@ namespace MuzikaGromche
|
|||
|
||||
void load()
|
||||
{
|
||||
var palette = Plugin.CurrentTrack?._Palette ?? Palette.DEFAULT;
|
||||
var palette = (Plugin.CurrentTrack as CoreAudioTrack)?._Palette ?? Palette.DEFAULT;
|
||||
var colors = palette.Colors;
|
||||
var count = Math.Min(colors.Count(), maxCustomPaletteSize);
|
||||
|
||||
|
@ -1859,7 +2080,7 @@ namespace MuzikaGromche
|
|||
const string section = "Timings";
|
||||
var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f);
|
||||
// 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<float> fadeOutBeatSyncedEntry = null!;
|
||||
SyncedEntry<float> fadeOutDurationSyncedEntry = null!;
|
||||
|
@ -1918,12 +2139,12 @@ namespace MuzikaGromche
|
|||
registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = 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);
|
||||
syncedEntry.SyncHostToLocal();
|
||||
syncedEntry.Changed += (sender, args) => applier();
|
||||
void loader(Track? track)
|
||||
void loader(CoreAudioTrack? track)
|
||||
{
|
||||
// if track is null, set everything to defaults
|
||||
syncedEntry.LocalValue = track == null ? (T)syncedEntry.Entry.DefaultValue : getter(track);
|
||||
|
@ -1931,11 +2152,11 @@ namespace MuzikaGromche
|
|||
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));
|
||||
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));
|
||||
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,
|
||||
(track) => string.Join(", ", getter(track)),
|
||||
() =>
|
||||
|
@ -1969,7 +2190,7 @@ namespace MuzikaGromche
|
|||
var track = Plugin.CurrentTrack;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.Load(track);
|
||||
entry.Load(track as CoreAudioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2016,6 +2237,11 @@ namespace MuzikaGromche
|
|||
|
||||
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()
|
||||
{
|
||||
ChooseTrackDeferred();
|
||||
|
@ -2042,6 +2268,7 @@ namespace MuzikaGromche
|
|||
|
||||
private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
|
||||
{
|
||||
SelectedTrackIndex = 0;
|
||||
ChooseTrackDeferred();
|
||||
}
|
||||
|
||||
|
@ -2072,13 +2299,15 @@ namespace MuzikaGromche
|
|||
[ServerRpc]
|
||||
public void ChooseTrackServerRpc()
|
||||
{
|
||||
var track = Plugin.ChooseTrack();
|
||||
Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {track.Name}");
|
||||
SetTrackClientRpc(track.Name);
|
||||
var selectableTrack = Plugin.ChooseTrack();
|
||||
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
|
||||
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
|
||||
[HarmonyPatch(typeof(JesterAI))]
|
||||
static class JesterPatch
|
||||
|
@ -2109,7 +2338,7 @@ namespace MuzikaGromche
|
|||
};
|
||||
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
|
||||
// which we don't care about stopping for now.
|
||||
//
|
||||
|
@ -2146,7 +2375,7 @@ namespace MuzikaGromche
|
|||
|
||||
// Override popGoesTheWeaselTheme with Start audio
|
||||
__instance.farAudio.maxDistance = Plugin.AudioMaxDistance;
|
||||
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedStart;
|
||||
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro;
|
||||
__instance.farAudio.loop = false;
|
||||
if (Config.ShouldSkipWindingPhase)
|
||||
{
|
||||
|
@ -2161,13 +2390,15 @@ namespace MuzikaGromche
|
|||
}
|
||||
__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)
|
||||
{
|
||||
Plugin.ResetLightColor();
|
||||
DiscoBallManager.Disable();
|
||||
// Rotate track groups
|
||||
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc();
|
||||
}
|
||||
|
||||
if (__instance.previousState == 2 && __state.previousState != 2)
|
||||
|
@ -2176,22 +2407,22 @@ namespace MuzikaGromche
|
|||
__instance.farAudio = __state.farAudio;
|
||||
|
||||
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.maxDistance = Plugin.AudioMaxDistance;
|
||||
__instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop;
|
||||
__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}");
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
switch (ev)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "MuzikaGromche",
|
||||
"version_number": "1337.420.69",
|
||||
"version_number": "1337.420.9001",
|
||||
"author": "Ratijas",
|
||||
"description": "Add some content to your inverse teleporter experience on Titan!",
|
||||
"website_url": "https://git.vilunov.me/ratijas/muzika-gromche",
|
||||
|
|
Loading…
Reference in New Issue