Implement client-side playback with Vanilla Compat Mode
This commit is contained in:
parent
dcae12ab36
commit
b8ef4d7937
|
|
@ -3,6 +3,7 @@
|
||||||
## MuzikaGromche 1337.9001.67
|
## MuzikaGromche 1337.9001.67
|
||||||
|
|
||||||
- Added a new track TwoFastTuFurious (from the same artist as PickUpSticks), thematic to the upcoming Valentine's Day.
|
- Added a new track TwoFastTuFurious (from the same artist as PickUpSticks), thematic to the upcoming Valentine's Day.
|
||||||
|
- Added support for client-side playback while playing with an unmodded/vanilla host.
|
||||||
|
|
||||||
## MuzikaGromche 1337.9001.4 - v73 Chinese New Year Edition
|
## MuzikaGromche 1337.9001.4 - v73 Chinese New Year Edition
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1070,9 +1070,8 @@ namespace MuzikaGromche
|
||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ISelectableTrack ChooseTrack()
|
static (ISelectableTrack[], Season?) GetTracksAndSeason()
|
||||||
{
|
{
|
||||||
var seed = GetCurrentSeed();
|
|
||||||
var today = DateTime.Today;
|
var today = DateTime.Today;
|
||||||
var season = SeasonalContentManager.CurrentSeason(today);
|
var season = SeasonalContentManager.CurrentSeason(today);
|
||||||
var tracksEnumerable = SeasonalContentManager.Filter(Tracks, season);
|
var tracksEnumerable = SeasonalContentManager.Filter(Tracks, season);
|
||||||
|
|
@ -1081,6 +1080,13 @@ namespace MuzikaGromche
|
||||||
tracksEnumerable = tracksEnumerable.Where(track => !track.IsExplicit);
|
tracksEnumerable = tracksEnumerable.Where(track => !track.IsExplicit);
|
||||||
}
|
}
|
||||||
var tracks = tracksEnumerable.ToArray();
|
var tracks = tracksEnumerable.ToArray();
|
||||||
|
return (tracks, season);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ISelectableTrack ChooseTrack()
|
||||||
|
{
|
||||||
|
var seed = GetCurrentSeed();
|
||||||
|
var (tracks, season) = GetTracksAndSeason();
|
||||||
int[] weights = tracks.Select(track => track.Weight.Value).ToArray();
|
int[] weights = tracks.Select(track => track.Weight.Value).ToArray();
|
||||||
var rwi = new RandomWeightedIndex(weights);
|
var rwi = new RandomWeightedIndex(weights);
|
||||||
var trackId = rwi.GetRandomWeightedIndex(seed);
|
var trackId = rwi.GetRandomWeightedIndex(seed);
|
||||||
|
|
@ -1089,6 +1095,54 @@ namespace MuzikaGromche
|
||||||
return tracks[trackId];
|
return tracks[trackId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This range results in 23 out of 33 tracks (70%) being selectable with the lowest overlap of 35% in the vanilla 35-40 seconds range.
|
||||||
|
internal const float CompatModeAllowLongerTrack = 3f; // audio may start earlier (and last longer) than vanilla timer
|
||||||
|
internal const float CompatModeAllowShorterTrack = 3f; // audio may start later (and last shorter) than vanilla timer
|
||||||
|
|
||||||
|
// Select the track whose wind-up timer most closely matches target vanilla value,
|
||||||
|
// so that we have a bit of leeway to delay the intro or start playing it earlier to match vanilla pop-up timing.
|
||||||
|
public static IAudioTrack? ChooseTrackCompat(float vanillaPopUpTimer)
|
||||||
|
{
|
||||||
|
var seed = GetCurrentSeed();
|
||||||
|
var (tracks, season) = GetTracksAndSeason();
|
||||||
|
// Don't just select the closest match, select from a range of them!
|
||||||
|
var minTimer = vanillaPopUpTimer - CompatModeAllowShorterTrack;
|
||||||
|
var maxTimer = vanillaPopUpTimer + CompatModeAllowLongerTrack;
|
||||||
|
bool TimerIsCompatible(IAudioTrack t) => minTimer <= t.WindUpTimer && t.WindUpTimer <= maxTimer;
|
||||||
|
|
||||||
|
// Similar to RandomWeightedIndex:
|
||||||
|
// If everything is set to zero, everything is equally possible
|
||||||
|
var allWeightsAreZero = tracks.All(t => t.Weight.Value == 0);
|
||||||
|
bool WeightIsCompatible(ISelectableTrack t) => allWeightsAreZero || t.Weight.Value > 0;
|
||||||
|
|
||||||
|
var compatibleSelectableTracks = tracks
|
||||||
|
.Where(track => WeightIsCompatible(track) && track.GetTracks().Any(TimerIsCompatible))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (compatibleSelectableTracks.Length == 0)
|
||||||
|
{
|
||||||
|
Log.LogWarning($"Seed is {seed}, season is {season?.Name ?? "<none>"}, no compat tracks found for timer {vanillaPopUpTimer}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select track group where at least one track member is compatible
|
||||||
|
int[] weights = compatibleSelectableTracks.Select(track => track.Weight.Value).ToArray();
|
||||||
|
var rwi = new RandomWeightedIndex(weights);
|
||||||
|
var trackId = rwi.GetRandomWeightedIndex(seed);
|
||||||
|
var selectableTrack = compatibleSelectableTracks[trackId];
|
||||||
|
|
||||||
|
// Select only compatible members from the selected group
|
||||||
|
var compatibleAudioTracks = selectableTrack.GetTracks().Where(TimerIsCompatible).ToArray();
|
||||||
|
|
||||||
|
// Randomly choose a compatible member from the group
|
||||||
|
var rng = new System.Random(seed + (int)(vanillaPopUpTimer * 1000));
|
||||||
|
var groupIndex = rng.Next();
|
||||||
|
var audioTrack = Mod.Index(compatibleAudioTracks, groupIndex);
|
||||||
|
|
||||||
|
Log.LogInfo($"Seed is {seed}, season is {season?.Name ?? "<none>"}, chosen compat track is \"{audioTrack.Name}\" with timer: {audioTrack.WindUpTimer}, vanilla timer: {vanillaPopUpTimer}");
|
||||||
|
return audioTrack;
|
||||||
|
}
|
||||||
|
|
||||||
public static IAudioTrack? FindTrackNamed(string name)
|
public static IAudioTrack? FindTrackNamed(string name)
|
||||||
{
|
{
|
||||||
return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name);
|
return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name);
|
||||||
|
|
@ -2487,6 +2541,7 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
public static bool ExtrapolateTime { get; private set; } = true;
|
public static bool ExtrapolateTime { get; private set; } = true;
|
||||||
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
public static bool ShouldSkipWindingPhase { get; private set; } = false;
|
||||||
|
public static bool VanillaCompatMode { get; private set; } = false;
|
||||||
|
|
||||||
// Audio files are normalized to target -14 LUFS, which is too loud to communicate. Reduce by another -9 dB down to about -23 LUFS.
|
// Audio files are normalized to target -14 LUFS, which is too loud to communicate. Reduce by another -9 dB down to about -23 LUFS.
|
||||||
private const float VolumeDefault = 0.35f;
|
private const float VolumeDefault = 0.35f;
|
||||||
|
|
@ -2573,6 +2628,7 @@ namespace MuzikaGromche
|
||||||
SetupEntriesToSkipWinding(configFile);
|
SetupEntriesToSkipWinding(configFile);
|
||||||
SetupEntriesForPaletteOverride(configFile);
|
SetupEntriesForPaletteOverride(configFile);
|
||||||
SetupEntriesForTimingsOverride(configFile);
|
SetupEntriesForTimingsOverride(configFile);
|
||||||
|
SetupEntriesForVanillaCompatMode(configFile);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var chanceRange = new AcceptableValueRange<int>(0, 100);
|
var chanceRange = new AcceptableValueRange<int>(0, 100);
|
||||||
|
|
@ -2644,7 +2700,7 @@ namespace MuzikaGromche
|
||||||
private void SetupEntriesToSkipWinding(ConfigFile configFile)
|
private void SetupEntriesToSkipWinding(ConfigFile configFile)
|
||||||
{
|
{
|
||||||
var entry = configFile.Bind("General", "Skip Winding Phase", false,
|
var entry = configFile.Bind("General", "Skip Winding Phase", false,
|
||||||
new ConfigDescription("Skip most of the wind-up/intro 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.\n\nDoes not work in Vanilla Compat Mode."));
|
||||||
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, requiresRestart: false));
|
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, requiresRestart: false));
|
||||||
entry.SettingChanged += (sender, args) => apply();
|
entry.SettingChanged += (sender, args) => apply();
|
||||||
apply();
|
apply();
|
||||||
|
|
@ -2655,6 +2711,20 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetupEntriesForVanillaCompatMode(ConfigFile configFile)
|
||||||
|
{
|
||||||
|
var entry = configFile.Bind("General", "Vanilla Compat Mode", false,
|
||||||
|
new ConfigDescription("DO NOT ENABLE! Disables networking / synchronization!\n\nKeep vanilla wind-up timer, select tracks whose timer is close to vanilla.\n\nMay cause the audio to start playing earlier or later.\n\nIf you join a vanilla host you are always in compat mode."));
|
||||||
|
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, requiresRestart: false));
|
||||||
|
entry.SettingChanged += (sender, args) => apply();
|
||||||
|
apply();
|
||||||
|
|
||||||
|
void apply()
|
||||||
|
{
|
||||||
|
VanillaCompatMode = entry.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SetupEntriesForPaletteOverride(ConfigFile configFile)
|
private void SetupEntriesForPaletteOverride(ConfigFile configFile)
|
||||||
{
|
{
|
||||||
const string section = "Palette";
|
const string section = "Palette";
|
||||||
|
|
@ -3097,26 +3167,71 @@ namespace MuzikaGromche
|
||||||
DeferredCoroutine = StartCoroutine(ChooseTrackDeferredCoroutine());
|
DeferredCoroutine = StartCoroutine(ChooseTrackDeferredCoroutine());
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator ChooseTrackDeferredCoroutine()
|
// Public API to rotate tracks, throttled
|
||||||
|
public void ChooseTrack()
|
||||||
|
{
|
||||||
|
ChooseTrackDeferred();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HostSetTrack = false;
|
||||||
|
// Playing with modded host automatically disables vanilla compatability mode
|
||||||
|
public bool VanillaCompatMode => IsHost ? Config.VanillaCompatMode : !HostSetTrack;
|
||||||
|
|
||||||
|
IEnumerator ChooseTrackDeferredCoroutine()
|
||||||
{
|
{
|
||||||
yield return new WaitForEndOfFrame();
|
yield return new WaitForEndOfFrame();
|
||||||
DeferredCoroutine = null;
|
DeferredCoroutine = null;
|
||||||
ChooseTrackServerRpc();
|
|
||||||
|
// Attempt to call server RPC on host, but set timeout and fallback to client-side vanilla compat mode.
|
||||||
|
// Host is never running in compat mode.
|
||||||
|
HostSetTrack = false;
|
||||||
|
|
||||||
|
// vanilla compat host should skip networking, making it client-side only
|
||||||
|
if (!Config.VanillaCompatMode)
|
||||||
|
{
|
||||||
|
ChooseTrackServerRpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternatively, there could be another RPC to inform clients of host's capabilities when joining the lobby.
|
||||||
|
// If host sets a track later, it would override the locally-selected one.
|
||||||
|
// The only downside of false-positive eager loading is the overhead of loading
|
||||||
|
// an extra pair of audio files and keeping them in cache until the end of round.
|
||||||
|
const float HostTimeout = 1f;
|
||||||
|
yield return new WaitForSeconds(HostTimeout);
|
||||||
|
|
||||||
|
if (!HostSetTrack)
|
||||||
|
{
|
||||||
|
Plugin.Log.LogInfo("Host did not set track in time, choosing locally");
|
||||||
|
var vanillaPopUpTimer = gameObject.GetComponent<JesterAI>().popUpTimer;
|
||||||
|
var audioTrack = Plugin.ChooseTrackCompat(vanillaPopUpTimer);
|
||||||
|
// it is important to reset any previous track if no new compatible one is found
|
||||||
|
SetTrack(audioTrack?.Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ClientRpc]
|
[ClientRpc]
|
||||||
public void SetTrackClientRpc(string name)
|
void SetTrackClientRpc(string name)
|
||||||
{
|
{
|
||||||
Plugin.Log.LogInfo($"SetTrackClientRpc {name}");
|
SetTrack(name);
|
||||||
if (Plugin.FindTrackNamed(name) is { } track)
|
HostSetTrack = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetTrack(string? name)
|
||||||
|
{
|
||||||
|
Plugin.Log.LogInfo($"SetTrack {name ?? "<none>"}");
|
||||||
|
if (name != null && Plugin.FindTrackNamed(name) is { } track)
|
||||||
{
|
{
|
||||||
AudioClipsCacheManager.LoadAudioTrack(track);
|
AudioClipsCacheManager.LoadAudioTrack(track);
|
||||||
CurrentTrack = Config.OverrideCurrentTrack(track);
|
CurrentTrack = Config.OverrideCurrentTrack(track);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CurrentTrack = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServerRpc]
|
[ServerRpc]
|
||||||
public void ChooseTrackServerRpc()
|
void ChooseTrackServerRpc()
|
||||||
{
|
{
|
||||||
var selectableTrack = Plugin.ChooseTrack();
|
var selectableTrack = Plugin.ChooseTrack();
|
||||||
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
|
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
|
||||||
|
|
@ -3125,9 +3240,9 @@ namespace MuzikaGromche
|
||||||
SelectedTrackIndex += 1;
|
SelectedTrackIndex += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void PlayScheduledLoop()
|
internal void PlayScheduledLoop(double introStartDspTime)
|
||||||
{
|
{
|
||||||
double loopStartDspTime = AudioSettings.dspTime + IntroAudioSource.clip.length - IntroAudioSource.time;
|
double loopStartDspTime = introStartDspTime + IntroAudioSource.clip.length - IntroAudioSource.time;
|
||||||
LoopAudioSource.PlayScheduled(loopStartDspTime);
|
LoopAudioSource.PlayScheduled(loopStartDspTime);
|
||||||
Plugin.Log.LogDebug($"Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}");
|
Plugin.Log.LogDebug($"Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}");
|
||||||
}
|
}
|
||||||
|
|
@ -3156,7 +3271,7 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// Almost instant follow timer
|
// Almost instant follow timer
|
||||||
__instance.beginCrankingTimer = 1f;
|
__instance.beginCrankingTimer = 1f + Plugin.CompatModeAllowLongerTrack;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3206,11 +3321,42 @@ namespace MuzikaGromche
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var vanillaCompatMode = behaviour.VanillaCompatMode;
|
||||||
|
// If greater than zero, delay the intro to start playing later.
|
||||||
|
// If less than zero, start playing before the beginCrankingTimer runs out.
|
||||||
|
var introDelta = vanillaCompatMode ? __instance.popUpTimer - behaviour.CurrentTrack.WindUpTimer : 0;
|
||||||
|
|
||||||
// This switch statement resembles the one from JesterAI.Update
|
// This switch statement resembles the one from JesterAI.Update
|
||||||
switch (__state.currentBehaviourStateIndex)
|
switch (__state.currentBehaviourStateIndex)
|
||||||
{
|
{
|
||||||
|
case 0:
|
||||||
|
// implied vanillaCompatMode
|
||||||
|
if (__instance.targetPlayer != null && introDelta < 0 && __instance.beginCrankingTimer < -introDelta)
|
||||||
|
{
|
||||||
|
if (!introAudioSource.isPlaying)
|
||||||
|
{
|
||||||
|
introAudioSource.clip = behaviour.CurrentTrack.LoadedIntro;
|
||||||
|
loopAudioSource.clip = behaviour.CurrentTrack.LoadedLoop;
|
||||||
|
behaviour.BeatTimeState = new BeatTimeState(behaviour.CurrentTrack);
|
||||||
|
|
||||||
|
// custom popup timer and config skip are not supported in compat mode
|
||||||
|
|
||||||
|
// reset if previously skipped winding by assigning different starting time.
|
||||||
|
introAudioSource.time = 0f;
|
||||||
|
|
||||||
|
__instance.farAudio.Stop();
|
||||||
|
var introStartDspTime = AudioSettings.dspTime;
|
||||||
|
introAudioSource.PlayScheduled(introStartDspTime);
|
||||||
|
behaviour.PlayScheduledLoop(introStartDspTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
if (__state.previousState != 1)
|
if (__state.previousState != 1 && introDelta < 0)
|
||||||
|
{
|
||||||
|
__instance.farAudio.Stop();
|
||||||
|
}
|
||||||
|
if (__state.previousState != 1 && introDelta >= 0)
|
||||||
{
|
{
|
||||||
// if just started winding up
|
// if just started winding up
|
||||||
// then stop the default music... (already done above)
|
// then stop the default music... (already done above)
|
||||||
|
|
@ -3219,10 +3365,13 @@ namespace MuzikaGromche
|
||||||
loopAudioSource.clip = behaviour.CurrentTrack.LoadedLoop;
|
loopAudioSource.clip = behaviour.CurrentTrack.LoadedLoop;
|
||||||
behaviour.BeatTimeState = new BeatTimeState(behaviour.CurrentTrack);
|
behaviour.BeatTimeState = new BeatTimeState(behaviour.CurrentTrack);
|
||||||
|
|
||||||
// Set up custom popup timer, which is shorter than Intro audio
|
if (!vanillaCompatMode)
|
||||||
__instance.popUpTimer = behaviour.CurrentTrack.WindUpTimer;
|
{
|
||||||
|
// Set up custom popup timer, which is shorter than Intro audio
|
||||||
|
__instance.popUpTimer = behaviour.CurrentTrack.WindUpTimer;
|
||||||
|
}
|
||||||
|
|
||||||
if (Config.ShouldSkipWindingPhase)
|
if (Config.ShouldSkipWindingPhase && !vanillaCompatMode)
|
||||||
{
|
{
|
||||||
var rewind = 5f;
|
var rewind = 5f;
|
||||||
__instance.popUpTimer = rewind;
|
__instance.popUpTimer = rewind;
|
||||||
|
|
@ -3235,8 +3384,9 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
|
|
||||||
__instance.farAudio.Stop();
|
__instance.farAudio.Stop();
|
||||||
introAudioSource.Play();
|
var introStartDspTime = AudioSettings.dspTime + introDelta;
|
||||||
behaviour.PlayScheduledLoop();
|
introAudioSource.PlayScheduled(introStartDspTime);
|
||||||
|
behaviour.PlayScheduledLoop(introStartDspTime);
|
||||||
}
|
}
|
||||||
if (__instance.stunNormalizedTimer > 0f)
|
if (__instance.stunNormalizedTimer > 0f)
|
||||||
{
|
{
|
||||||
|
|
@ -3248,8 +3398,9 @@ namespace MuzikaGromche
|
||||||
if (!introAudioSource.isPlaying)
|
if (!introAudioSource.isPlaying)
|
||||||
{
|
{
|
||||||
__instance.farAudio.Stop();
|
__instance.farAudio.Stop();
|
||||||
|
var introStartDspTime = AudioSettings.dspTime + introDelta;
|
||||||
introAudioSource.UnPause();
|
introAudioSource.UnPause();
|
||||||
behaviour.PlayScheduledLoop();
|
behaviour.PlayScheduledLoop(introStartDspTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -3268,7 +3419,7 @@ namespace MuzikaGromche
|
||||||
DiscoBallManager.Disable();
|
DiscoBallManager.Disable();
|
||||||
ScreenFiltersManager.Clear();
|
ScreenFiltersManager.Clear();
|
||||||
// Rotate track groups
|
// Rotate track groups
|
||||||
behaviour.ChooseTrackServerRpc();
|
behaviour.ChooseTrack();
|
||||||
behaviour.BeatTimeState = null;
|
behaviour.BeatTimeState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
_Add some content to your Inverse teleporter experience on Titan!<sup>1</sup>_
|
_Add some content to your Inverse teleporter experience on Titan!<sup>1</sup>_
|
||||||
|
|
||||||
Muzika Gromche literally means _"crank music louder"_. This mod replaces Jester's winding up and chasing sounds with **a whole library** of timed to the beat and **seamlessly looped** popular energetic songs, combined with various **visual effects**. Song choice is random each day but **synchronized** with clients: everyone in the lobby will wibe to the same tunes.
|
Muzika Gromche literally means _"crank music louder"_. This mod replaces Jester's winding up and chasing sounds with **a whole library** of timed to the beat and **seamlessly looped** popular energetic songs, combined with various **visual effects**. Song choice is random each day but **synchronized** with clients: everyone in the lobby will wibe to the same tunes, however **vanilla-compatible** client-side playback is also supported.
|
||||||
|
|
||||||
A demo video is worth a thousand words. Check out what Muzika Gromche does:
|
A demo video is worth a thousand words. Check out what Muzika Gromche does:
|
||||||
|
|
||||||
|
|
@ -16,6 +16,9 @@ An example of trapping One Punch Man themed Jester at mineshaft:
|
||||||
|
|
||||||
Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes Jester's wind-up timers to pop up on the drop, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.*
|
Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes Jester's wind-up timers to pop up on the drop, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.*
|
||||||
|
|
||||||
|
- **Modded host is compatible** with vanilla clients, but if a selected track's wind-up timer differs significantly from the vanilla value, vanilla clients may observe Jester pop early or hear its theme restart from the beginning.
|
||||||
|
- **Modded client is compatible** with vanilla host: the client will locally select the track whose wind-up timer most closely matches vanilla value, but it will never play tracks whose timer fall far outside of vanilla range.
|
||||||
|
|
||||||
Muzika Gromche v1337.9001.0 has been updated to work with Lethal Company v73. Previous versions of Muzika Gromche work with all Lethal Company versions from all the way back to v40 and up to v72.
|
Muzika Gromche v1337.9001.0 has been updated to work with Lethal Company v73. Previous versions of Muzika Gromche work with all Lethal Company versions from all the way back to v40 and up to v72.
|
||||||
|
|
||||||
- [`LobbyCompatibility`] is recommended but optional.
|
- [`LobbyCompatibility`] is recommended but optional.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue