1
0
Fork 0

Compare commits

...

7 Commits

Author SHA1 Message Date
ivan tkachenko 6a9ea8d4af Release v1337.420.9001 2025-08-14 19:17:35 +03:00
ivan tkachenko 42c6179ba5 Add new track Beha with three variants of intro 2025-08-14 19:13:20 +03:00
ivan tkachenko 5649a18633 Split Track into Selectable and Audio interfaces, add support for groups 2025-08-14 18:48:54 +03:00
ivan tkachenko 47f984cd28 Allow tracks to share common audio clip files
Send one request per file name. File names can be explicitly overridden.
2025-08-14 15:38:59 +03:00
ivan tkachenko fc3a62e511 Rename Start segment to Intro to reduce some confusion
Confusingly, "start" may refer to too many things in different places,
while "intro" would unambiguously refer to an audio clip that plays
first before the loop starts.
2025-08-14 15:11:46 +03:00
ivan tkachenko 5f0c890682 Remove unused method 2025-08-14 15:09:31 +03:00
ivan tkachenko 59a069f51b Bump version 2025-08-14 15:09:27 +03:00
24 changed files with 371 additions and 123 deletions

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -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

View File

@ -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" />

View File

@ -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]);
#if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} Track {track.Name} {track.LoadedStart.length:N4} {track.LoadedLoop.length:N4}");
#endif
var clip = DownloadHandlerAudioClip.GetContent(tuple.Request);
foreach (var setter in tuple.Setters)
{
setter(clip);
}
}
#if DEBUG
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)

View File

@ -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",