forked from nikita/muzika-gromche
Compare commits
No commits in common. "df796965f2b9f77a64d03955a5d3387bbf5ed675" and "8b2f4428bb97118b42d9c6e7c4dcd4482846dbdd" have entirely different histories.
df796965f2
...
8b2f4428bb
|
@ -1,5 +0,0 @@
|
|||
[*.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)
BIN
Assets/BeefLiverLoop.ogg (Stored with Git LFS)
Binary file not shown.
BIN
Assets/BeefLiverStart.ogg (Stored with Git LFS)
BIN
Assets/BeefLiverStart.ogg (Stored with Git LFS)
Binary file not shown.
|
@ -1,11 +1,5 @@
|
|||
# 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.
|
||||
|
|
|
@ -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.69.420</Version>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
@ -41,7 +41,6 @@
|
|||
<!--
|
||||
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" />
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using CSync.Extensions;
|
||||
using CSync.Lib;
|
||||
using HarmonyLib;
|
||||
using LethalConfig;
|
||||
using LethalConfig.ConfigItems;
|
||||
|
@ -19,17 +21,10 @@ 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)]
|
||||
|
@ -475,27 +470,6 @@ 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()
|
||||
|
@ -555,9 +529,6 @@ 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++)
|
||||
|
@ -578,9 +549,6 @@ 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();
|
||||
|
@ -857,15 +825,11 @@ namespace MuzikaGromche
|
|||
// Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative.
|
||||
public readonly 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)
|
||||
public BeatTimestamp(int loopBeats, bool isLooping, float beat)
|
||||
{
|
||||
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)
|
||||
|
@ -878,7 +842,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, self.IsExtrapolated);
|
||||
return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta);
|
||||
}
|
||||
|
||||
public static BeatTimestamp operator -(BeatTimestamp self, float delta)
|
||||
|
@ -890,12 +854,12 @@ namespace MuzikaGromche
|
|||
{
|
||||
// There is no way it wraps or affects IsLooping state
|
||||
var beat = Mathf.Floor(Beat);
|
||||
return new BeatTimestamp(LoopBeats, IsLooping, beat, IsExtrapolated);
|
||||
return new BeatTimestamp(LoopBeats, IsLooping, beat);
|
||||
}
|
||||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')} {Beat:N4}/{LoopBeats})";
|
||||
return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')} {Beat:N4}/{LoopBeats})";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -908,16 +872,13 @@ 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, bool isExtrapolated)
|
||||
public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive)
|
||||
{
|
||||
LoopBeats = loopBeats;
|
||||
IsLooping = isLooping || beatToInclusive >= HalfLoopBeats;
|
||||
BeatFromExclusive = wrap(beatFromExclusive);
|
||||
BeatToInclusive = wrap(beatToInclusive);
|
||||
IsExtrapolated = isExtrapolated;
|
||||
|
||||
float wrap(float beat)
|
||||
{
|
||||
|
@ -927,20 +888,20 @@ namespace MuzikaGromche
|
|||
|
||||
public static BeatTimeSpan Between(BeatTimestamp timestampFromExclusive, BeatTimestamp timestampToInclusive)
|
||||
{
|
||||
var isExtrapolated = timestampFromExclusive.IsExtrapolated || timestampToInclusive.IsExtrapolated;
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat, isExtrapolated);
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat);
|
||||
}
|
||||
|
||||
|
||||
public static BeatTimeSpan Between(float beatFromExclusive, BeatTimestamp timestampToInclusive)
|
||||
{
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat, timestampToInclusive.IsExtrapolated);
|
||||
return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat);
|
||||
}
|
||||
|
||||
public static BeatTimeSpan Empty = new();
|
||||
|
||||
public readonly BeatTimestamp ToTimestamp()
|
||||
{
|
||||
return new(LoopBeats, IsLooping, BeatToInclusive, IsExtrapolated);
|
||||
return new(LoopBeats, IsLooping, BeatToInclusive);
|
||||
}
|
||||
|
||||
// The beat will not be wrapped.
|
||||
|
@ -962,7 +923,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, IsExtrapolated);
|
||||
var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive);
|
||||
var laterIndex = laterSpan.GetLastIndex(timeSeries);
|
||||
if (laterIndex != null)
|
||||
{
|
||||
|
@ -1048,144 +1009,94 @@ namespace MuzikaGromche
|
|||
|
||||
public readonly override string ToString()
|
||||
{
|
||||
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
|
||||
return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})";
|
||||
}
|
||||
}
|
||||
|
||||
class ExtrapolatedAudioSourceState
|
||||
class BeatTimeState
|
||||
{
|
||||
// AudioSource.isPlaying
|
||||
public bool IsPlaying { get; private set; }
|
||||
// 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.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+Loop/2.
|
||||
private bool windUpOffsetIsLooping = 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;
|
||||
// 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;
|
||||
|
||||
public bool IsExtrapolated => LastKnownNonExtrapolatedTime != ExtrapolatedTime;
|
||||
private bool windUpZeroBeatEventTriggered = false;
|
||||
|
||||
private float ExtrapolatedTime = 0f;
|
||||
private readonly Track track;
|
||||
|
||||
private float LastKnownNonExtrapolatedTime = 0f;
|
||||
private float loopOffsetBeat = float.NegativeInfinity;
|
||||
|
||||
// Any wall clock based measurements of when this state was recorded
|
||||
private float LastKnownRealtime = 0f;
|
||||
private static System.Random lyricsRandom = null!;
|
||||
|
||||
private const float MaxExtrapolationInterval = 0.5f;
|
||||
private int lyricsRandomPerLoop;
|
||||
|
||||
public void Update(AudioSource audioSource, float realtime)
|
||||
public BeatTimeState(Track track)
|
||||
{
|
||||
IsPlaying = audioSource.isPlaying;
|
||||
HasStarted |= audioSource.time != 0f;
|
||||
|
||||
if (LastKnownNonExtrapolatedTime != audioSource.time)
|
||||
if (lyricsRandom == null)
|
||||
{
|
||||
LastKnownRealtime = realtime;
|
||||
LastKnownNonExtrapolatedTime = ExtrapolatedTime = audioSource.time;
|
||||
lyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337);
|
||||
lyricsRandomPerLoop = lyricsRandom.Next();
|
||||
}
|
||||
// Frames are rendering faster than AudioSource updates its playback time state
|
||||
else if (IsPlaying && HasStarted && Config.ExtrapolateTime)
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
public List<BaseEvent> Update(AudioSource start, AudioSource loop)
|
||||
{
|
||||
hasStarted |= start.time != 0;
|
||||
if (hasStarted)
|
||||
{
|
||||
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.Assert(LastKnownNonExtrapolatedTime == audioSource.time); // implied
|
||||
Debug.Log($"{nameof(MuzikaGromche)} looping? {(loopOffsetIsLooping ? 'X' : '_')}{(windUpOffsetIsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} events={string.Join(",", events)}");
|
||||
#endif
|
||||
var deltaTime = realtime - LastKnownRealtime;
|
||||
if (0 < deltaTime && deltaTime < MaxExtrapolationInterval)
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Events other than colors start rotating at 0=WindUpTimer+LoopOffset.
|
||||
private BeatTimestamp UpdateStateForLoopOffset(AudioSource start, AudioSource loop)
|
||||
{
|
||||
ExtrapolatedTime = LastKnownNonExtrapolatedTime + deltaTime;
|
||||
}
|
||||
}
|
||||
var offset = BaseOffset() + track.LoopOffsetInSeconds;
|
||||
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, loopOffsetIsLooping);
|
||||
loopOffsetIsLooping |= timestamp.IsLooping;
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
// Colors start rotating at 0=WindUpTimer
|
||||
private BeatTimestamp UpdateStateForWindUpOffset(AudioSource start, AudioSource loop)
|
||||
{
|
||||
IsPlaying = false;
|
||||
var offset = BaseOffset();
|
||||
var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, windUpOffsetIsLooping);
|
||||
windUpOffsetIsLooping |= timestamp.IsLooping;
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
private float BaseOffset()
|
||||
{
|
||||
return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} "
|
||||
+ (IsExtrapolated
|
||||
? $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4} => {ExtrapolatedTime:N4}"
|
||||
: $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4}"
|
||||
) + ")";
|
||||
}
|
||||
return Config.AudioOffset.Value + track.BeatsOffsetInSeconds + track.WindUpTimer;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
StartClipLength = startClipLength;
|
||||
}
|
||||
|
||||
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)
|
||||
BeatTimestamp GetTimestampRelativeToGivenOffset(AudioSource start, AudioSource loop, float offset, bool isLooping)
|
||||
{
|
||||
// If popped, calculate which beat the music is currently at.
|
||||
// In order to do that we should choose one of two strategies:
|
||||
|
@ -1198,114 +1109,42 @@ 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 offset = StartOfLoop + additionalOffset;
|
||||
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;
|
||||
|
||||
float timeSinceStartOfLoop = time - offset;
|
||||
float adjustedTimeFromOffset = timeFromTheVeryStart - offset;
|
||||
|
||||
var adjustedTimeNormalized = timeSinceStartOfLoop / LoopLength;
|
||||
var adjustedTimeNormalized = adjustedTimeFromOffset / track.LoadedLoop.length;
|
||||
|
||||
var beat = adjustedTimeNormalized * Beats;
|
||||
var beat = adjustedTimeNormalized * track.Beats;
|
||||
|
||||
// Let it infer the isLooping flag from the beat
|
||||
var timestamp = new BeatTimestamp(Beats, IsLooping, beat, isExtrapolated);
|
||||
|
||||
IsLooping |= timestamp.IsLooping;
|
||||
var timestamp = new BeatTimestamp(track.Beats, isLooping, beat);
|
||||
|
||||
#if DEBUG && false
|
||||
Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} {3} Time={4:N4} norm={5,6:N4} beat={6,7:N4}",
|
||||
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}",
|
||||
nameof(MuzikaGromche),
|
||||
Time.realtimeSinceStartup, Time.deltaTime,
|
||||
isExtrapolated ? 'E' : '_', time,
|
||||
adjustedTimeNormalized, beat);
|
||||
(start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'),
|
||||
(loop.isPlaying ? '+' : ' '), loop.time,
|
||||
adjustedTimeNormalized, beat, color);
|
||||
#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)
|
||||
|
@ -1327,7 +1166,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 != "")
|
||||
{
|
||||
|
@ -1594,7 +1433,6 @@ namespace MuzikaGromche
|
|||
readonly public int TotalWeights { get; }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static class SyncedEntryExtensions
|
||||
{
|
||||
// Update local values on clients. Even though the clients couldn't
|
||||
|
@ -1607,12 +1445,8 @@ namespace MuzikaGromche
|
|||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
class Config
|
||||
#if DEBUG
|
||||
: SyncedConfig2<Config>
|
||||
#endif
|
||||
class Config : SyncedConfig2<Config>
|
||||
{
|
||||
public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!;
|
||||
|
||||
|
@ -1622,7 +1456,6 @@ 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;
|
||||
|
@ -1636,10 +1469,7 @@ namespace MuzikaGromche
|
|||
public static float? ColorTransitionOutOverride { get; private set; } = null;
|
||||
public static string? ColorTransitionEasingOverride { get; private set; } = null;
|
||||
|
||||
internal Config(ConfigFile configFile)
|
||||
#if DEBUG
|
||||
: base(PluginInfo.PLUGIN_GUID)
|
||||
#endif
|
||||
internal Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID)
|
||||
{
|
||||
DisplayLyrics = configFile.Bind("General", "Display Lyrics", true,
|
||||
new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music."));
|
||||
|
@ -1659,7 +1489,6 @@ namespace MuzikaGromche
|
|||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions())));
|
||||
|
||||
#if DEBUG
|
||||
SetupEntriesForExtrapolation(configFile);
|
||||
SetupEntriesToSkipWinding(configFile);
|
||||
SetupEntriesForPaletteOverride(configFile);
|
||||
SetupEntriesForTimingsOverride(configFile);
|
||||
|
@ -1708,12 +1537,9 @@ 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)
|
||||
|
@ -1721,7 +1547,6 @@ namespace MuzikaGromche
|
|||
// This is basically what ConfigFile.PopulateEntryContainer does
|
||||
EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase);
|
||||
}
|
||||
#endif
|
||||
|
||||
public static CanModifyResult CanModifyIfHost()
|
||||
{
|
||||
|
@ -1757,23 +1582,6 @@ 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,
|
||||
|
@ -1885,7 +1693,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, 10)));
|
||||
new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 100)));
|
||||
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", "",
|
||||
|
@ -1981,7 +1789,6 @@ namespace MuzikaGromche
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private T Default<T>(T options) where T : BaseOptions
|
||||
{
|
||||
|
@ -2021,30 +1828,15 @@ namespace MuzikaGromche
|
|||
ChooseTrackDeferred();
|
||||
foreach (var track in Plugin.Tracks)
|
||||
{
|
||||
track.Weight.SettingChanged += ChooseTrackDeferredDelegate;
|
||||
track.Weight.SettingChanged += (_, _) => ChooseTrackDeferred();
|
||||
}
|
||||
Config.SkipExplicitTracks.SettingChanged += ChooseTrackDeferredDelegate;
|
||||
Config.SkipExplicitTracks.SettingChanged += (_, _) => ChooseTrackDeferred();
|
||||
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)
|
||||
|
@ -2176,7 +1968,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();
|
||||
|
@ -2189,9 +1981,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 is { } beatTimeState)
|
||||
if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState != null)
|
||||
{
|
||||
var events = beatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
|
||||
var events = Plugin.BeatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice);
|
||||
foreach (var ev in events)
|
||||
{
|
||||
switch (ev)
|
||||
|
|
|
@ -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 ([`LethalConfig`] and [`LobbyCompatibility`]) 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 ([`CSync`] and [`LethalConfig`]) 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,5 +38,4 @@ 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/
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "MuzikaGromche",
|
||||
"version_number": "1337.420.69",
|
||||
"version_number": "1337.69.420",
|
||||
"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"
|
||||
|
|
Loading…
Reference in New Issue