1
0
Fork 0

Compare commits

...

9 Commits

Author SHA1 Message Date
ivan tkachenko df796965f2 Release v1337.420.69 2025-08-11 22:28:57 +03:00
ivan tkachenko 26f9d2cf9f Print tracks length in debug builds, and remove unnecessary non-null assertion 2025-08-11 22:28:32 +03:00
ivan tkachenko a950093f8e Sort tracks by name, so they are easier to find in the config 2025-08-11 22:28:32 +03:00
ivan tkachenko 8842005898 Add new track BeefLiver 2025-08-11 22:28:31 +03:00
ivan tkachenko b4ae4bad41 Config: More usable range for fading out 2025-08-11 22:28:31 +03:00
ivan tkachenko 69e64397a0 Extrapolate AudioSource playback time to get smoother transitions
AudioSource only updates about 25 times per second, meaning that even at
30 fps some adjacent frames would be calculated as having exact same
timestamps and render duplicated colors. At 100+ fps more than 2/3 of
the frames would be duplicates.

As a drive-by change, split complex logic of BeatTimeState into smaller
classes. Most of the time the state needs to maintain some boolean flag
which it flips once and stays that way, like HasStarted, IsLooping.
2025-08-11 22:28:31 +03:00
ivan tkachenko 3d0795f04d Drop CSync as a dependency from Release builds
Since the rewrite of track selection to a custom netcode, CSync is only
needed for debug/development builds now.
2025-08-11 22:28:31 +03:00
ivan tkachenko 4abd0fb612 Fix stale event handlers causing errors in console 2025-08-11 22:28:30 +03:00
ivan tkachenko dd3c9647e3 Bump version 2025-08-11 22:28:29 +03:00
8 changed files with 324 additions and 98 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.cs]
# IDE0290: Use primary constructor
# Primary constructors are far from perfect: they can't have readonly fields, while fields can be used anywhere in the class body.
csharp_style_prefer_primary_constructors = false

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

Binary file not shown.

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

Binary file not shown.

View File

@ -1,5 +1,11 @@
# Changelog
## MuzikaGromche 1337.420.69 - It's All DiscoNnected Edition
- Fix harmless but annoying errors in BepInEx console output.
- Improve smoothness of color animations.
- Add a new track.
## MuzikaGromche 1337.69.420 - It's All Connected Edition
- Fix certain object hanging around after being disabled.

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.69.420</Version>
<Version>1337.420.69</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
@ -41,6 +41,7 @@
<!--
Publicize internal methods, so we could generate config entries for tracks at runtime instead
of generating code at compile time. See https://github.com/lc-sigurd/CSync/issues/11
It is an optional dependency now, but there is no sane way to mark it as such.
-->
<PackageReference Include="Sigurd.BepInEx.CSync" Version="5.0.1" Publicize="true" PrivateAssets="all" Private="false" />
<PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />

View File

@ -1,7 +1,5 @@
using BepInEx;
using BepInEx.Configuration;
using CSync.Extensions;
using CSync.Lib;
using HarmonyLib;
using LethalConfig;
using LethalConfig.ConfigItems;
@ -21,10 +19,17 @@ using Unity.Netcode;
using UnityEngine;
using UnityEngine.Networking;
#if DEBUG
using CSync.Extensions;
using CSync.Lib;
#endif
namespace MuzikaGromche
{
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
#if DEBUG
[BepInDependency("com.sigurd.csync", "5.0.1")]
#endif
[BepInDependency("ainavt.lc.lethalconfig", "1.4.6")]
[BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)]
[BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.HardDependency)]
@ -470,6 +475,27 @@ namespace MuzikaGromche
FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45],
Lyrics = [],
},
new Track
{
Name = "BeefLiver",
AudioType = AudioType.OGGVORBIS,
Language = Language.ENGLISH,
WindUpTimer = 39.35f,
Bars = 12,
BeatsOffset = 0.2f,
ColorTransitionIn = 0.4f,
ColorTransitionOut = 0.4f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse([
"#FFEBEB", "#FFEBEB", "#445782", "#EBA602",
"#5EEBB9", "#8EE3DC", "#A23045", "#262222",
]),
LoopOffset = 0,
FadeOutBeat = -3,
FadeOutDuration = 3,
FlickerLightsTimeSeries = [-48, -40, -4.5f, 44],
Lyrics = [],
},
];
public static Track ChooseTrack()
@ -529,6 +555,9 @@ namespace MuzikaGromche
void Awake()
{
// Sort in place by name
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++)
@ -549,6 +578,9 @@ namespace MuzikaGromche
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
}
Config = new Config(base.Config);
DiscoBallManager.Load();
@ -825,11 +857,15 @@ namespace MuzikaGromche
// Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative.
public readonly float Beat;
public BeatTimestamp(int loopBeats, bool isLooping, float beat)
// Additional metadata describing whether this timestamp is based on extrapolated source data.
public readonly bool IsExtrapolated;
public BeatTimestamp(int loopBeats, bool isLooping, float beat, bool isExtrapolated)
{
LoopBeats = loopBeats;
IsLooping = isLooping || beat >= HalfLoopBeats;
Beat = isLooping || beat >= LoopBeats ? Mod.Positive(beat, LoopBeats) : beat;
IsExtrapolated = isExtrapolated;
}
public static BeatTimestamp operator +(BeatTimestamp self, float delta)
@ -842,7 +878,7 @@ namespace MuzikaGromche
// Shouldn't be needed though, as deltas are usually short enough.
// But don't try to chain many short negative deltas!
}
return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta);
return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta, self.IsExtrapolated);
}
public static BeatTimestamp operator -(BeatTimestamp self, float delta)
@ -854,12 +890,12 @@ namespace MuzikaGromche
{
// There is no way it wraps or affects IsLooping state
var beat = Mathf.Floor(Beat);
return new BeatTimestamp(LoopBeats, IsLooping, beat);
return new BeatTimestamp(LoopBeats, IsLooping, beat, IsExtrapolated);
}
public readonly override string ToString()
{
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')} {Beat:N4}/{LoopBeats})";
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')} {Beat:N4}/{LoopBeats})";
}
}
@ -872,13 +908,16 @@ namespace MuzikaGromche
public readonly float BeatFromExclusive;
// Closed upper bound
public readonly float BeatToInclusive;
// Additional metadata describing whether this timestamp is based on extrapolated source data.
public readonly bool IsExtrapolated;
public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive)
public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive, bool isExtrapolated)
{
LoopBeats = loopBeats;
IsLooping = isLooping || beatToInclusive >= HalfLoopBeats;
BeatFromExclusive = wrap(beatFromExclusive);
BeatToInclusive = wrap(beatToInclusive);
IsExtrapolated = isExtrapolated;
float wrap(float beat)
{
@ -888,20 +927,20 @@ namespace MuzikaGromche
public static BeatTimeSpan Between(BeatTimestamp timestampFromExclusive, BeatTimestamp timestampToInclusive)
{
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat);
var isExtrapolated = timestampFromExclusive.IsExtrapolated || timestampToInclusive.IsExtrapolated;
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat, isExtrapolated);
}
public static BeatTimeSpan Between(float beatFromExclusive, BeatTimestamp timestampToInclusive)
{
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat);
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat, timestampToInclusive.IsExtrapolated);
}
public static BeatTimeSpan Empty = new();
public readonly BeatTimestamp ToTimestamp()
{
return new(LoopBeats, IsLooping, BeatToInclusive);
return new(LoopBeats, IsLooping, BeatToInclusive, IsExtrapolated);
}
// The beat will not be wrapped.
@ -923,7 +962,7 @@ namespace MuzikaGromche
// before wrapping (happens earlier) and after wrapping (happens later).
// Check the "happens later" part first.
var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive);
var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive, IsExtrapolated);
var laterIndex = laterSpan.GetLastIndex(timeSeries);
if (laterIndex != null)
{
@ -1009,94 +1048,144 @@ namespace MuzikaGromche
public readonly override string ToString()
{
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
}
}
class BeatTimeState
class ExtrapolatedAudioSourceState
{
// The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet.
private bool hasStarted = false;
// AudioSource.isPlaying
public bool IsPlaying { get; private set; }
// The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+Loop/2.
private bool windUpOffsetIsLooping = false;
// AudioSource.time, possibly extrapolated
public float Time => ExtrapolatedTime;
// The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+LoopOffset+Loop/2.
private bool loopOffsetIsLooping = false;
// The object is newly created, the AudioSource began to play (possibly delayed) but its time hasn't advanced from 0.0f yet.
// Time can not be extrapolated when HasStarted is false.
public bool HasStarted { get; private set; } = false;
private bool windUpZeroBeatEventTriggered = false;
public bool IsExtrapolated => LastKnownNonExtrapolatedTime != ExtrapolatedTime;
private readonly Track track;
private float ExtrapolatedTime = 0f;
private float loopOffsetBeat = float.NegativeInfinity;
private float LastKnownNonExtrapolatedTime = 0f;
private static System.Random lyricsRandom = null!;
// Any wall clock based measurements of when this state was recorded
private float LastKnownRealtime = 0f;
private int lyricsRandomPerLoop;
private const float MaxExtrapolationInterval = 0.5f;
public BeatTimeState(Track track)
public void Update(AudioSource audioSource, float realtime)
{
if (lyricsRandom == null)
IsPlaying = audioSource.isPlaying;
HasStarted |= audioSource.time != 0f;
if (LastKnownNonExtrapolatedTime != audioSource.time)
{
lyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337);
lyricsRandomPerLoop = lyricsRandom.Next();
LastKnownRealtime = realtime;
LastKnownNonExtrapolatedTime = ExtrapolatedTime = audioSource.time;
}
this.track = track;
}
public List<BaseEvent> Update(AudioSource start, AudioSource loop)
{
hasStarted |= start.time != 0;
if (hasStarted)
// Frames are rendering faster than AudioSource updates its playback time state
else if (IsPlaying && HasStarted && Config.ExtrapolateTime)
{
var loopTimestamp = UpdateStateForLoopOffset(start, loop);
var loopOffsetSpan = BeatTimeSpan.Between(loopOffsetBeat, loopTimestamp);
// Do not go back in time
if (!loopOffsetSpan.IsEmpty())
{
if (loopOffsetSpan.BeatFromExclusive > loopOffsetSpan.BeatToInclusive)
{
lyricsRandomPerLoop = lyricsRandom.Next();
}
var windUpOffsetTimestamp = UpdateStateForWindUpOffset(start, loop);
loopOffsetBeat = loopTimestamp.Beat;
var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp);
#if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} looping? {(loopOffsetIsLooping ? 'X' : '_')}{(windUpOffsetIsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} events={string.Join(",", events)}");
Debug.Assert(LastKnownNonExtrapolatedTime == audioSource.time); // implied
#endif
return events;
var deltaTime = realtime - LastKnownRealtime;
if (0 < deltaTime && deltaTime < MaxExtrapolationInterval)
{
ExtrapolatedTime = LastKnownNonExtrapolatedTime + deltaTime;
}
}
return [];
}
// Events other than colors start rotating at 0=WindUpTimer+LoopOffset.
private BeatTimestamp UpdateStateForLoopOffset(AudioSource start, AudioSource loop)
public void Finish()
{
var offset = BaseOffset() + track.LoopOffsetInSeconds;
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, loopOffsetIsLooping);
loopOffsetIsLooping |= timestamp.IsLooping;
return timestamp;
IsPlaying = false;
}
// Colors start rotating at 0=WindUpTimer
private BeatTimestamp UpdateStateForWindUpOffset(AudioSource start, AudioSource loop)
public override string ToString()
{
var offset = BaseOffset();
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, windUpOffsetIsLooping);
windUpOffsetIsLooping |= timestamp.IsLooping;
return timestamp;
return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} "
+ (IsExtrapolated
? $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4} => {ExtrapolatedTime:N4}"
: $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4}"
) + ")";
}
}
private float BaseOffset()
class JesterAudioSourcesState
{
private readonly float StartClipLength;
// 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,
// 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 Loop = new();
// If true, use Start state as a reference, otherwise use Loop.
private bool ReferenceIsStart = true;
public bool HasStarted => Start.HasStarted;
public bool IsExtrapolated => ReferenceIsStart ? Start.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 JesterAudioSourcesState(float startClipLength)
{
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds + track.WindUpTimer;
StartClipLength = startClipLength;
}
BeatTimestamp GetTimestampRelativeToGivenOffset(AudioSource start, AudioSource loop, float offset, bool isLooping)
public void Update(AudioSource start, AudioSource loop, float realtime)
{
// It doesn't make sense to update start state after loop has started (because start.isPlaying occasionally becomes true).
// But always makes sense to update loop, so we can check if it has actually started.
Loop.Update(loop, realtime);
if (!Loop.HasStarted)
{
#if DEBUG
Debug.Assert(ReferenceIsStart);
#endif
Start.Update(start, realtime);
}
else
{
ReferenceIsStart = false;
}
}
}
// This class tracks looping state of the playback, so that the timestamps can be correctly wrapped only when needed.
// [... ...time... ...]
// ^ |
// `---|---' loop
// ^ IsLooping becomes true and stays true forever.
class AudioLoopingState
{
public bool IsLooping { get; private set; } = false;
private readonly float StartOfLoop;
private readonly float LoopLength;
private readonly int Beats;
public AudioLoopingState(float startOfLoop, float loopLength, int beats)
{
StartOfLoop = startOfLoop;
LoopLength = loopLength;
Beats = beats;
}
public BeatTimestamp Update(float time, bool isExtrapolated, float additionalOffset)
{
// If popped, calculate which beat the music is currently at.
// In order to do that we should choose one of two strategies:
@ -1109,42 +1198,114 @@ namespace MuzikaGromche
// 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.
var timeFromTheVeryStart = start.isPlaying && start.time != 0f
// [1] Start source is still playing
? start.time
// [2] Start source has finished
: track.LoadedStart.length + loop.time;
var offset = StartOfLoop + additionalOffset;
float adjustedTimeFromOffset = timeFromTheVeryStart - offset;
float timeSinceStartOfLoop = time - offset;
var adjustedTimeNormalized = adjustedTimeFromOffset / track.LoadedLoop.length;
var adjustedTimeNormalized = timeSinceStartOfLoop / LoopLength;
var beat = adjustedTimeNormalized * track.Beats;
var beat = adjustedTimeNormalized * Beats;
// Let it infer the isLooping flag from the beat
var timestamp = new BeatTimestamp(track.Beats, isLooping, beat);
var timestamp = new BeatTimestamp(Beats, IsLooping, beat, isExtrapolated);
IsLooping |= timestamp.IsLooping;
#if DEBUG && false
var color = ColorFromPaletteAtTimestamp(timestamp);
Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} Start[{3}{4,8:N4} zero? {5}] Loop[{6}{7,8:N4}] norm={8,6:N4} beat={9,7:N4} color={10}",
Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} {3} Time={4:N4} norm={5,6:N4} beat={6,7:N4}",
nameof(MuzikaGromche),
Time.realtimeSinceStartup, Time.deltaTime,
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
(loop.isPlaying ? '+' : ' '), loop.time,
adjustedTimeNormalized, beat, color);
isExtrapolated ? 'E' : '_', time,
adjustedTimeNormalized, beat);
#endif
return timestamp;
}
}
class BeatTimeState
{
private readonly Track track;
private readonly JesterAudioSourcesState AudioState;
// Colors wrap from WindUpTimer
private readonly AudioLoopingState WindUpLoopingState;
// Events other than colors wrap from WindUpTimer+LoopOffset.
private readonly AudioLoopingState LoopLoopingState;
private float LastKnownLoopOffsetBeat = float.NegativeInfinity;
private static System.Random LyricsRandom = null!;
private int LyricsRandomPerLoop;
private bool WindUpZeroBeatEventTriggered = false;
public BeatTimeState(Track track)
{
if (LyricsRandom == null)
{
LyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337);
LyricsRandomPerLoop = LyricsRandom.Next();
}
this.track = track;
AudioState = new(track.LoadedStart.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)
{
var time = Time.realtimeSinceStartup;
AudioState.Update(start, loop, time);
if (AudioState.HasStarted)
{
var loopTimestamp = Update(LoopLoopingState);
var loopOffsetSpan = BeatTimeSpan.Between(LastKnownLoopOffsetBeat, loopTimestamp);
// Do not go back in time
if (!loopOffsetSpan.IsEmpty())
{
if (loopOffsetSpan.BeatFromExclusive > loopOffsetSpan.BeatToInclusive)
{
LyricsRandomPerLoop = LyricsRandom.Next();
}
var windUpOffsetTimestamp = Update(WindUpLoopingState);
LastKnownLoopOffsetBeat = loopTimestamp.Beat;
var events = GetEvents(loopOffsetSpan, windUpOffsetTimestamp);
#if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}");
#endif
return events;
}
}
return [];
}
private BeatTimestamp Update(AudioLoopingState loopingState)
{
return loopingState.Update(AudioState.Time, AudioState.IsExtrapolated, AdditionalOffset());
}
// Timings that may be changes through config
private float AdditionalOffset()
{
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds;
}
private List<BaseEvent> GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp)
{
List<BaseEvent> events = [];
if (windUpOffsetTimestamp.Beat >= 0f && !windUpZeroBeatEventTriggered)
if (windUpOffsetTimestamp.Beat >= 0f && !WindUpZeroBeatEventTriggered)
{
events.Add(new WindUpZeroBeatEvent());
windUpZeroBeatEventTriggered = true;
WindUpZeroBeatEventTriggered = true;
}
if (GetColorEvent(loopOffsetSpan, windUpOffsetTimestamp) is { } colorEvent)
@ -1166,7 +1327,7 @@ namespace MuzikaGromche
{
var line = track.LyricsLines[i];
var alternatives = line.Split('\t');
var randomIndex = lyricsRandomPerLoop % alternatives.Length;
var randomIndex = LyricsRandomPerLoop % alternatives.Length;
var alternative = alternatives[randomIndex];
if (alternative != "")
{
@ -1433,6 +1594,7 @@ namespace MuzikaGromche
readonly public int TotalWeights { get; }
}
#if DEBUG
static class SyncedEntryExtensions
{
// Update local values on clients. Even though the clients couldn't
@ -1445,8 +1607,12 @@ namespace MuzikaGromche
};
}
}
#endif
class Config : SyncedConfig2<Config>
class Config
#if DEBUG
: SyncedConfig2<Config>
#endif
{
public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!;
@ -1456,6 +1622,7 @@ namespace MuzikaGromche
public static ConfigEntry<bool> OverrideSpawnRates { get; private set; } = null!;
public static bool ExtrapolateTime { get; private set; } = true;
public static bool ShouldSkipWindingPhase { get; private set; } = false;
public static Palette? PaletteOverride { get; private set; } = null;
@ -1469,7 +1636,10 @@ namespace MuzikaGromche
public static float? ColorTransitionOutOverride { get; private set; } = null;
public static string? ColorTransitionEasingOverride { get; private set; } = null;
internal Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID)
internal Config(ConfigFile configFile)
#if DEBUG
: base(PluginInfo.PLUGIN_GUID)
#endif
{
DisplayLyrics = configFile.Bind("General", "Display Lyrics", true,
new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music."));
@ -1489,6 +1659,7 @@ namespace MuzikaGromche
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions())));
#if DEBUG
SetupEntriesForExtrapolation(configFile);
SetupEntriesToSkipWinding(configFile);
SetupEntriesForPaletteOverride(configFile);
SetupEntriesForTimingsOverride(configFile);
@ -1537,9 +1708,12 @@ namespace MuzikaGromche
LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight, Default(new IntSliderOptions())));
}
#if DEBUG
ConfigManager.Register(this);
#endif
}
#if DEBUG
// HACK because CSync doesn't provide an API to register a list of config entries
// See https://github.com/lc-sigurd/CSync/issues/11
private void CSyncHackAddSyncedEntry(SyncedEntryBase entryBase)
@ -1547,6 +1721,7 @@ namespace MuzikaGromche
// This is basically what ConfigFile.PopulateEntryContainer does
EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase);
}
#endif
public static CanModifyResult CanModifyIfHost()
{
@ -1582,6 +1757,23 @@ namespace MuzikaGromche
return CanModifyResult.True();
}
#if DEBUG
private void SetupEntriesForExtrapolation(ConfigFile configFile)
{
var syncedEntry = configFile.BindSyncedEntry("General", "Extrapolate Audio Playback Time", true,
new ConfigDescription("AudioSource only updates its playback position about 20 times per second.\n\nUse extrapolation technique to predict playback time between updates for smoother color animations."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(syncedEntry.Entry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(syncedEntry);
syncedEntry.Changed += (sender, args) => apply();
syncedEntry.SyncHostToLocal();
apply();
void apply()
{
ExtrapolateTime = syncedEntry.Value;
}
}
private void SetupEntriesToSkipWinding(ConfigFile configFile)
{
var syncedEntry = configFile.BindSyncedEntry("General", "Skip Winding Phase", false,
@ -1693,7 +1885,7 @@ namespace MuzikaGromche
fadeOutBeatSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Beat", 0f,
new ConfigDescription("The beat at which to start fading out", new AcceptableValueRange<float>(-1000f, 0)));
fadeOutDurationSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Duration", 0f,
new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 100)));
new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10)));
flickerLightsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Flicker Lights Time Series", "",
new ConfigDescription("Time series of beat offsets when to flicker the lights."));
lyricsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Lyrics Time Series", "",
@ -1789,6 +1981,7 @@ namespace MuzikaGromche
}
}
}
#endif
private T Default<T>(T options) where T : BaseOptions
{
@ -1828,15 +2021,30 @@ namespace MuzikaGromche
ChooseTrackDeferred();
foreach (var track in Plugin.Tracks)
{
track.Weight.SettingChanged += (_, _) => ChooseTrackDeferred();
track.Weight.SettingChanged += ChooseTrackDeferredDelegate;
}
Config.SkipExplicitTracks.SettingChanged += (_, _) => ChooseTrackDeferred();
Config.SkipExplicitTracks.SettingChanged += ChooseTrackDeferredDelegate;
base.OnNetworkSpawn();
}
public override void OnNetworkDespawn()
{
foreach (var track in Plugin.Tracks)
{
track.Weight.SettingChanged -= ChooseTrackDeferredDelegate;
}
Config.SkipExplicitTracks.SettingChanged -= ChooseTrackDeferredDelegate;
base.OnNetworkDespawn();
}
// Batch multiple weights changes in a single network RPC
private Coroutine? DeferredCoroutine = null;
private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
{
ChooseTrackDeferred();
}
private void ChooseTrackDeferred()
{
if (DeferredCoroutine != null)
@ -1968,7 +2176,7 @@ namespace MuzikaGromche
__instance.farAudio = __state.farAudio;
var time = __instance.farAudio.time;
var delay = Plugin.CurrentTrack!.LoadedStart.length - time;
var delay = Plugin.CurrentTrack.LoadedStart.length - time;
// Override screamingSFX with Loop, delayed by the remaining time of the Start audio
__instance.creatureVoice.Stop();
@ -1981,9 +2189,9 @@ namespace MuzikaGromche
}
// Manage the timeline: switch color of the lights according to the current playback/beat position.
if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState != null)
if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState is { } beatTimeState)
{
var events = Plugin.BeatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
var events = beatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
foreach (var ev in events)
{
switch (ev)

View File

@ -9,7 +9,7 @@ To keep it a surprise, it is adviced that you do not read the detailed descripti
Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes certain timers, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.*
Muzika Gromche works with all Lethal Company versions from v72 all the way back to v40, and is likely to work on all future versions as long as dependencies ([`CSync`] and [`LethalConfig`]) are working.
Muzika Gromche works with all Lethal Company versions from v72 all the way back to v40, and is likely to work on all future versions as long as dependencies ([`LethalConfig`] and [`LobbyCompatibility`]) are working.
Speaking of dependencies, [`V70PoweredLights_Fix`] is not strictly required, but it doesn't hurt to have it installed on any version, and it makes this mod more enjoyable on new Mansion tiles.
@ -38,4 +38,5 @@ Any player can change their personal preferences locally.
[`CSync`]: https://thunderstore.io/c/lethal-company/p/Sigurd/CSync/
[`LethalConfig`]: https://thunderstore.io/c/lethal-company/p/AinaVT/LethalConfig/
[`LobbyCompatibility`]: https://thunderstore.io/c/lethal-company/p/BMX/LobbyCompatibility/
[`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/

View File

@ -1,12 +1,11 @@
{
"name": "MuzikaGromche",
"version_number": "1337.69.420",
"version_number": "1337.420.69",
"author": "Ratijas",
"description": "Add some content to your inverse teleporter experience on Titan!",
"website_url": "https://git.vilunov.me/ratijas/muzika-gromche",
"dependencies": [
"BepInEx-BepInExPack-5.4.2100",
"Sigurd-CSync-5.0.1",
"AinaVT-LethalConfig-1.4.6",
"WaterGun-V70PoweredLights_Fix-1.0.0",
"BMX-LobbyCompatibility-1.5.1"