@ -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 BeatTim eState
class ExtrapolatedAudioSourc eState
{
// The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet.
p rivate bool hasStarted = false ;
// AudioSource.isPlaying
p ublic 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.
p rivate bool windUpOffsetIsLooping = fals e;
// AudioSource.time, possibly extrapolated
p ublic float Time = > ExtrapolatedTim e;
// 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 ;
p rivate bool windUpZeroBeatEventTriggered = fals e;
p ublic bool IsExtrapolated = > LastKnownNonExtrapolatedTime ! = ExtrapolatedTim e;
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 . l ength;
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 & & ! W indUpZeroBeatEventTriggered)
{
events . Add ( new WindUpZeroBeatEvent ( ) ) ;
windUpZeroBeatEventTriggered = true ;
W indUpZeroBeatEventTriggered = 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 = l yricsRandomPerLoop % alternatives . Length ;
var randomIndex = L yricsRandomPerLoop % 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 , 10 0 ) ) ) ;
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. B eatTimeState. Update ( start : __instance . farAudio , loop : __instance . creatureVoice ) ;
var events = b eatTimeState. Update ( start : __instance . farAudio , loop : __instance . creatureVoice ) ;
foreach ( var ev in events )
{
switch ( ev )