1
0
Fork 0

Compare commits

...

17 Commits

Author SHA1 Message Date
ivan tkachenko 63de62111f Release v1337.420.9002 2025-08-23 02:34:17 +03:00
ivan tkachenko 4cc9713fa7 Fix resetting to wrong initial colors, e.g. in Mineshaft tunnel tiles
This does not fix fading out and transitioning to the very first palette
color though, but fixing that would require color events to
be "personalized" per-light, which is currently not supported.
2025-08-23 01:49:12 +03:00
ivan tkachenko 8710df7525 Change config value for Override Spawn Rates to true by default 2025-08-22 16:09:01 +03:00
ivan tkachenko 9d23fd5b95 Downgrade LobbyCompatibility to optional dependency
Since it does not prevent unmodded clients from joining, there is no
reason for literally any mod to require it.
2025-08-22 16:05:42 +03:00
ivan tkachenko 4516b853cd Remove remaining CSync code and references
There were issues with clients not being able to join, potentially
caused by linked (even though actually unused) CSync library.
2025-08-22 15:16:24 +03:00
ivan tkachenko b3767cbbf0 Add "polyfill" for IsExternalInit C# feature
Imperium does this as well, and the whole internet would tell you to do
this too, so it should be fine.
2025-08-22 15:16:24 +03:00
ivan tkachenko 327e606deb Drop required properties syntax
Sometimes, seemingly after random unrelated changes, it might stop
compiling with internal compiler error messages about missing features
and attributes. .NET Standard 2.1 is not supposed to support any
features beyond C# 8.0, while `required` attribute was introduced only
in C# 11 or 12, it's hard to tell.
2025-08-22 15:16:24 +03:00
ivan tkachenko 70e45d5ba2 Remove unused class 2025-08-22 15:16:24 +03:00
ivan tkachenko d4d3e15de3 Clean separation between track data and config overrides
In debug builds Config keeps a reference to the last set original track
instance from which it can load original values.
2025-08-22 15:16:23 +03:00
ivan tkachenko 525c0e108f Refactor CurrentTrack to be less dependent on a global static 2025-08-22 15:16:23 +03:00
ivan tkachenko 73ad702684 Rewrite AudioSource handling from scratch 2025-08-22 15:16:23 +03:00
ivan tkachenko e67de4556c Move BeatTimeState from global static to per-Jester-instance Behaviour 2025-08-22 15:16:11 +03:00
ivan tkachenko 0b0383003f Reset BeatTimeState for good measure
Hopefully will fix Mineshaft lights somehow getting stuck in multiplayer.
2025-08-22 15:16:11 +03:00
ivan tkachenko 9ed98197f8 Remaster track Beha and BeefLiver at conventional 44100 Hz 2025-08-22 15:16:09 +03:00
ivan tkachenko fe5752cbff Remaster track Beha and BeefLiver at conventional 44100 Hz 2025-08-21 15:30:47 +03:00
ivan tkachenko c6b128270f Add new track OnePartiyaUdar 2025-08-15 00:52:38 +03:00
ivan tkachenko 852d866073 Bump version 2025-08-15 00:51:53 +03:00
12 changed files with 392 additions and 315 deletions

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -1,5 +1,15 @@
# Changelog
## MuzikaGromche 1337.420.9002 - Anime Edition
- Added a new track OnePartiyaUdar in Japanese language.
- Remastered recently added tracks at conventional 44100 Hz for better stitching.
- Improved playback experience: use precise DSP time and up-front scheduing for seamless audio stitching, add custom Audio Sources to improve reliability.
- Removed remaining CSync code and package references even from debug builds.
- Downgraded LobbyCompatibility to optional dependency.
- Toggled config option to increase certain spawn rate to ON by default.
- Fixed resetting to wrong initial colors, e.g. in Mineshaft tunnel tiles.
## MuzikaGromche 1337.420.9001 - Multiverse Edition
- Added support for tracks to rotate between multiple audio variants during a round.

View File

@ -0,0 +1,26 @@
using BepInEx;
using BepInEx.Bootstrap;
using LobbyCompatibility.Enums;
using LobbyCompatibility.Features;
using System.Runtime.CompilerServices;
namespace MuzikaGromche
{
internal static class Compatibility
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Register(BaseUnityPlugin plugin)
{
if (Chainloader.PluginInfos.ContainsKey("BMX.LobbyCompatibility"))
{
RegisterLobbyCompatibility(plugin.Info.Metadata);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RegisterLobbyCompatibility(BepInPlugin plugin)
{
PluginHelper.RegisterPlugin(plugin.GUID, plugin.Version, CompatibilityLevel.Everyone, VersionStrictness.Patch);
}
}
}

View File

@ -0,0 +1,6 @@
// ReSharper disable once CheckNamespace
#pragma warning disable IDE0130 // Namespace does not match folder structure
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit;
}

View File

@ -8,7 +8,7 @@
<AssemblyName>Ratijas.MuzikaGromche</AssemblyName>
<Product>Muzika Gromche</Product>
<Description>Add some content to your inverse teleporter experience on Titan!</Description>
<Version>1337.420.9001</Version>
<Version>1337.420.9002</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
@ -38,12 +38,6 @@
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="UnityEngine.Modules" Version="2022.3.9" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" Private="false" />
<!--
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" />
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
</ItemGroup>

View File

@ -4,8 +4,6 @@ using HarmonyLib;
using LethalConfig;
using LethalConfig.ConfigItems;
using LethalConfig.ConfigItems.Options;
using LobbyCompatibility.Attributes;
using LobbyCompatibility.Enums;
using System;
using System.Collections;
using System.Collections.Generic;
@ -19,25 +17,20 @@ 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)]
[LobbyCompatibility(CompatibilityLevel.Everyone, VersionStrictness.Patch)]
[BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.SoftDependency)]
public class Plugin : BaseUnityPlugin
{
internal new static Config Config { get; private set; } = null!;
// Not all lights are white by default. For example, Mineshaft's neon light is green-ish.
// We don't have to care about Light objects lifetime, as Unity would internally destroy them on scene unload anyway.
internal static Dictionary<Light, Color> InitialLightsColors = [];
private static readonly string[] PwnLyricsVariants = [
"", "", "", // make sure the array has enough items to index it without checking
..NetworkInterface.GetAllNetworkInterfaces()
@ -565,6 +558,26 @@ namespace MuzikaGromche
},
],
},
new SelectableAudioTrack
{
Name = "OnePartiyaUdar",
AudioType = AudioType.OGGVORBIS,
Language = Language.JAPANESE,
WindUpTimer = 41.27f,
Bars = 12,
BeatsOffset = 0.3f,
ColorTransitionIn = 0.6f,
ColorTransitionOut = 0.15f,
ColorTransitionEasing = Easing.InOutExpo,
Palette = Palette.Parse([
"#9C3C37", "#E9BF5C", "#B5E3EA", "#662422", "#EBC3A8", "#AA8238",
]),
LoopOffset = 0,
FadeOutBeat = -8,
FadeOutDuration = 6,
FlickerLightsTimeSeries = [-68.5f, -16.5f, 30.5f],
Lyrics = [],
},
];
public static ISelectableTrack ChooseTrack()
@ -584,9 +597,6 @@ namespace MuzikaGromche
return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name);
}
internal static IAudioTrack? CurrentTrack;
internal static BeatTimeState? BeatTimeState;
public static void SetLightColor(Color color)
{
foreach (var light in RoundManager.Instance.allPoweredLights)
@ -597,7 +607,10 @@ namespace MuzikaGromche
public static void ResetLightColor()
{
SetLightColor(Color.white);
foreach (var (light, color) in InitialLightsColors)
{
light.color = color;
}
}
// Max audible distance for AudioSource and LyricsEvent
@ -684,6 +697,7 @@ namespace MuzikaGromche
harmony.PatchAll(typeof(DiscoBallDespawnPatch));
harmony.PatchAll(typeof(SpawnRatePatch));
NetcodePatcher();
Compatibility.Register(this);
}
else
{
@ -715,6 +729,7 @@ namespace MuzikaGromche
public static readonly Language ENGLISH = new("EN", "English");
public static readonly Language RUSSIAN = new("RU", "Russian");
public static readonly Language KOREAN = new("KO", "Korean");
public static readonly Language JAPANESE = new("JP", "Japanese");
public static readonly Language HINDI = new("HI", "Hindi");
}
@ -790,21 +805,6 @@ namespace MuzikaGromche
}
}
public struct SelectableTrackData()
{
// Name of the track, as shown in config entry UI; also used for default file names.
public required string Name { get; init; }
// Language of the track's lyrics.
public required Language Language { get; init; }
// Whether this track has NSFW/explicit lyrics.
public bool IsExplicit { get; init; } = false;
// How often this track should be chosen, relative to the sum of weights of all tracks.
public ConfigEntry<int> Weight { get; internal set; } = null!;
}
// An instance of a track which appears as a configuration entry and
// can be selected using weighted random from a list of selectable tracks.
public interface ISelectableTrack
@ -900,12 +900,37 @@ namespace MuzikaGromche
public Palette Palette { get; }
}
// A proxy audio track with default implementation for every IAudioTrack method that simply forwards requests to the inner IAudioTrack.
public abstract class ProxyAudioTrack(IAudioTrack track) : IAudioTrack
{
internal IAudioTrack Track = track;
string IAudioTrack.Name => Track.Name;
float IAudioTrack.WindUpTimer => Track.WindUpTimer;
int IAudioTrack.Beats => Track.Beats;
int IAudioTrack.LoopOffset => Track.LoopOffset;
AudioType IAudioTrack.AudioType => Track.AudioType;
AudioClip IAudioTrack.LoadedIntro { get => Track.LoadedIntro; set => Track.LoadedIntro = value; }
AudioClip IAudioTrack.LoadedLoop { get => Track.LoadedLoop; set => Track.LoadedLoop = value; }
string IAudioTrack.FileNameIntro => Track.FileNameIntro;
string IAudioTrack.FileNameLoop => Track.FileNameLoop;
float IAudioTrack.BeatsOffset => Track.BeatsOffset;
float IAudioTrack.FadeOutBeat => Track.FadeOutBeat;
float IAudioTrack.FadeOutDuration => Track.FadeOutDuration;
float IAudioTrack.ColorTransitionIn => Track.ColorTransitionIn;
float IAudioTrack.ColorTransitionOut => Track.ColorTransitionOut;
Easing IAudioTrack.ColorTransitionEasing => Track.ColorTransitionEasing;
float[] IAudioTrack.FlickerLightsTimeSeries => Track.FlickerLightsTimeSeries;
float[] IAudioTrack.LyricsTimeSeries => Track.LyricsTimeSeries;
string[] IAudioTrack.LyricsLines => Track.LyricsLines;
Palette IAudioTrack.Palette => Track.Palette;
}
// Core audio track implementation with some defaults and config overrides.
// Suitable to declare elemnents of SelectableTracksGroup and as a base for standalone selectable tracks.
public class CoreAudioTrack : IAudioTrack
{
public required string Name { get; init; }
public required float WindUpTimer { get; init; }
public /* required */ string Name { get; init; } = "";
public /* required */ float WindUpTimer { get; init; } = 0f;
public int Beats { get; init; }
// Shorthand for four beats
@ -933,56 +958,17 @@ namespace MuzikaGromche
init => FileNameLoopOverride = value;
}
public float _BeatsOffset = 0f;
public float BeatsOffset
{
get => Config.BeatsOffsetOverride ?? _BeatsOffset;
init => _BeatsOffset = value;
}
public float _FadeOutBeat = float.NaN;
public float FadeOutBeat
{
get => Config.FadeOutBeatOverride ?? _FadeOutBeat;
init => _FadeOutBeat = value;
}
public float _FadeOutDuration = 2f;
public float FadeOutDuration
{
get => Config.FadeOutDurationOverride ?? _FadeOutDuration;
init => _FadeOutDuration = value;
}
// Duration of color transition, measured in beats.
public float _ColorTransitionIn = 0.25f;
public float ColorTransitionIn
{
get => Config.ColorTransitionInOverride ?? _ColorTransitionIn;
init => _ColorTransitionIn = value;
}
public float _ColorTransitionOut = 0.25f;
public float ColorTransitionOut
{
get => Config.ColorTransitionOutOverride ?? _ColorTransitionOut;
init => _ColorTransitionOut = value;
}
// Easing function for color transitions.
public Easing _ColorTransitionEasing = Easing.OutExpo;
public Easing ColorTransitionEasing
{
get => Config.ColorTransitionEasingOverride != null
? Easing.FindByName(Config.ColorTransitionEasingOverride)
: _ColorTransitionEasing;
init => _ColorTransitionEasing = value;
}
public float BeatsOffset { get; init; } = 0f;
public float FadeOutBeat { get; init; } = float.NaN;
public float FadeOutDuration { get; init; } = 2f;
public float ColorTransitionIn { get; init; } = 0.25f;
public float ColorTransitionOut { get; init; } = 0.25f;
public Easing ColorTransitionEasing { get; init; } = Easing.OutExpo;
public float[] _FlickerLightsTimeSeries = [];
public float[] FlickerLightsTimeSeries
{
get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries;
get => _FlickerLightsTimeSeries;
init
{
Array.Sort(value);
@ -990,12 +976,7 @@ namespace MuzikaGromche
}
}
public float[] _LyricsTimeSeries = [];
public float[] LyricsTimeSeries
{
get => Config.LyricsTimeSeriesOverride ?? _LyricsTimeSeries;
private set => _LyricsTimeSeries = value;
}
public float[] LyricsTimeSeries { get; private set; } = [];
// Lyrics line may contain multiple tab-separated alternatives.
// In such case, a random number chosen and updated once per loop
@ -1016,18 +997,13 @@ namespace MuzikaGromche
}
}
public Palette _Palette = Palette.DEFAULT;
public Palette Palette
{
get => Config.PaletteOverride ?? _Palette;
set => _Palette = value;
}
public Palette Palette { get; set; } = Palette.DEFAULT;
}
// Standalone, top-level, selectable audio track
public class SelectableAudioTrack : CoreAudioTrack, ISelectableTrack
{
public required Language Language { get; init; }
public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
@ -1043,12 +1019,12 @@ namespace MuzikaGromche
public class SelectableTracksGroup : ISelectableTrack
{
public required string Name { get; init; }
public required Language Language { get; init; }
public /* required */ string Name { get; init; } = "";
public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
public required IAudioTrack[] Tracks;
public /* required */ IAudioTrack[] Tracks = [];
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
@ -1583,7 +1559,9 @@ namespace MuzikaGromche
if (windUpOffsetTimestamp.Beat < 0f && track.FadeOutBeat < loopOffsetSpan.BeatToInclusive && loopOffsetSpan.BeatFromExclusive <= fadeOutEnd)
{
var t = (loopOffsetSpan.BeatToInclusive - track.FadeOutBeat) / track.FadeOutDuration;
return new SetLightsColorTransitionEvent(Color.white, Color.black, Easing.Linear, t);
// TODO: assumes that default lights color is white
var DefaultLightsColor = Color.white;
return new SetLightsColorTransitionEvent(DefaultLightsColor, Color.black, Easing.Linear, t);
}
else
{
@ -1656,7 +1634,9 @@ namespace MuzikaGromche
}
else
{
return float.IsNaN(track.FadeOutBeat) ? Color.white : Color.black;
// TODO: assumes that default lights color is white
var DefaultLightsColor = Color.white;
return float.IsNaN(track.FadeOutBeat) ? DefaultLightsColor : Color.black;
}
}
}
@ -1815,25 +1795,7 @@ namespace MuzikaGromche
readonly public int TotalWeights { get; }
}
#if DEBUG
static class SyncedEntryExtensions
{
// Update local values on clients. Even though the clients couldn't
// edit them, they could at least see the new values.
public static void SyncHostToLocal<T>(this SyncedEntry<T> entry)
{
entry.Changed += (sender, args) =>
{
args.ChangedEntry.LocalValue = args.NewValue;
};
}
}
#endif
class Config
#if DEBUG
: SyncedConfig2<Config>
#endif
{
public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!;
@ -1846,21 +1808,47 @@ namespace MuzikaGromche
public static bool ExtrapolateTime { get; private set; } = true;
public static bool ShouldSkipWindingPhase { get; private set; } = false;
public static Palette? PaletteOverride { get; private set; } = null;
#if DEBUG
// Latest set track, used for loading palette and timings.
private static IAudioTrack? CurrentTrack = null;
// All per-track values that can be overridden
private static float? BeatsOffsetOverride = null;
private static float? FadeOutBeatOverride = null;
private static float? FadeOutDurationOverride = null;
private static float? ColorTransitionInOverride = null;
private static float? ColorTransitionOutOverride = null;
private static string? ColorTransitionEasingOverride = null;
private static float[]? FlickerLightsTimeSeriesOverride = null;
private static float[]? LyricsTimeSeriesOverride = null;
private static Palette? PaletteOverride = null;
public static float? FadeOutBeatOverride { get; private set; } = null;
public static float? FadeOutDurationOverride { get; private set; } = null;
public static float[]? FlickerLightsTimeSeriesOverride { get; private set; } = null;
public static float[]? LyricsTimeSeriesOverride { get; private set; } = null;
public static float? BeatsOffsetOverride { get; private set; } = null;
public static float? ColorTransitionInOverride { get; private set; } = null;
public static float? ColorTransitionOutOverride { get; private set; } = null;
public static string? ColorTransitionEasingOverride { get; private set; } = null;
private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack
{
float IAudioTrack.BeatsOffset => BeatsOffsetOverride ?? Track.BeatsOffset;
float IAudioTrack.FadeOutBeat => FadeOutBeatOverride ?? Track.FadeOutBeat;
float IAudioTrack.FadeOutDuration => FadeOutDurationOverride ?? Track.FadeOutDuration;
float IAudioTrack.ColorTransitionIn => ColorTransitionInOverride ?? Track.ColorTransitionIn;
float IAudioTrack.ColorTransitionOut => ColorTransitionOutOverride ?? Track.ColorTransitionOut;
Easing IAudioTrack.ColorTransitionEasing =>
ColorTransitionEasingOverride != null
? Easing.FindByName(ColorTransitionEasingOverride)
: Track.ColorTransitionEasing;
float[] IAudioTrack.FlickerLightsTimeSeries =>
FlickerLightsTimeSeriesOverride ?? Track.FlickerLightsTimeSeries;
float[] IAudioTrack.LyricsTimeSeries => LyricsTimeSeriesOverride ?? Track.LyricsTimeSeries;
Palette IAudioTrack.Palette => PaletteOverride ?? Track.Palette;
}
#endif
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."));
@ -1875,7 +1863,7 @@ namespace MuzikaGromche
new ConfigDescription("When choosing tracks at random, skip the ones with Explicit Content/Lyrics."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, Default(new BoolCheckBoxOptions())));
OverrideSpawnRates = configFile.Bind("General", "Override Spawn Rates", false,
OverrideSpawnRates = configFile.Bind("General", "Override Spawn Rates", true,
new ConfigDescription("Deviate from vanilla spawn rates to experience content of this mod more often."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions())));
@ -1928,21 +1916,17 @@ 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)
internal static IAudioTrack OverrideCurrentTrack(IAudioTrack track)
{
// This is basically what ConfigFile.PopulateEntryContainer does
EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase);
}
#if DEBUG
CurrentTrack = track;
return new AudioTrackWithConfigOverride(track);
#else
return track;
#endif
}
public static CanModifyResult CanModifyIfHost()
{
@ -1981,33 +1965,29 @@ namespace MuzikaGromche
#if DEBUG
private void SetupEntriesForExtrapolation(ConfigFile configFile)
{
var syncedEntry = configFile.BindSyncedEntry("General", "Extrapolate Audio Playback Time", true,
var entry = configFile.Bind("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();
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, Default(new BoolCheckBoxOptions())));
entry.SettingChanged += (sender, args) => apply();
apply();
void apply()
{
ExtrapolateTime = syncedEntry.Value;
ExtrapolateTime = entry.Value;
}
}
private void SetupEntriesToSkipWinding(ConfigFile configFile)
{
var syncedEntry = configFile.BindSyncedEntry("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."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(syncedEntry.Entry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(syncedEntry);
syncedEntry.Changed += (sender, args) => apply();
syncedEntry.SyncHostToLocal();
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, Default(new BoolCheckBoxOptions())));
entry.SettingChanged += (sender, args) => apply();
apply();
void apply()
{
ShouldSkipWindingPhase = syncedEntry.Value;
ShouldSkipWindingPhase = entry.Value;
}
}
@ -2016,60 +1996,56 @@ namespace MuzikaGromche
const string section = "Palette";
const int maxCustomPaletteSize = 8;
// Declare and initialize early to avoid "Use of unassigned local variable"
SyncedEntry<int> customPaletteSizeSyncedEntry = null!;
var customPaletteSyncedEntries = new SyncedEntry<string>[maxCustomPaletteSize];
ConfigEntry<int> customPaletteSizeEntry = null!;
var customPaletteEntries = new ConfigEntry<string>[maxCustomPaletteSize];
var loadButton = new GenericButtonConfigItem(section, "Load Palette from the Current Track",
"Override custom palette with the built-in palette of the current track.", "Load", load);
loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost;
LethalConfigManager.AddConfigItem(loadButton);
customPaletteSizeSyncedEntry = configFile.BindSyncedEntry(section, "Palette Size", 0, new ConfigDescription(
customPaletteSizeEntry = configFile.Bind(section, "Palette Size", 0, new ConfigDescription(
"Number of colors in the custom palette.\n\nIf set to non-zero, custom palette overrides track's own built-in palette.",
new AcceptableValueRange<int>(0, maxCustomPaletteSize)));
LethalConfigManager.AddConfigItem(new IntSliderConfigItem(customPaletteSizeSyncedEntry.Entry, Default(new IntSliderOptions())));
CSyncHackAddSyncedEntry(customPaletteSizeSyncedEntry);
customPaletteSizeSyncedEntry.Changed += (sender, args) => apply();
customPaletteSizeSyncedEntry.SyncHostToLocal();
LethalConfigManager.AddConfigItem(new IntSliderConfigItem(customPaletteSizeEntry, Default(new IntSliderOptions())));
customPaletteSizeEntry.SettingChanged += (sender, args) => apply();
for (int i = 0; i < maxCustomPaletteSize; i++)
{
string entryName = $"Custom Color {i + 1}";
var customColorSyncedEntry = configFile.BindSyncedEntry(section, entryName, "#FFFFFF", "Choose color for the custom palette");
customPaletteSyncedEntries[i] = customColorSyncedEntry;
LethalConfigManager.AddConfigItem(new HexColorInputFieldConfigItem(customColorSyncedEntry.Entry, Default(new HexColorInputFieldOptions())));
CSyncHackAddSyncedEntry(customColorSyncedEntry);
customColorSyncedEntry.Changed += (sender, args) => apply();
customColorSyncedEntry.SyncHostToLocal();
var customColorEntry = configFile.Bind(section, entryName, "#FFFFFF", "Choose color for the custom palette");
customPaletteEntries[i] = customColorEntry;
LethalConfigManager.AddConfigItem(new HexColorInputFieldConfigItem(customColorEntry, Default(new HexColorInputFieldOptions())));
customColorEntry.SettingChanged += (sender, args) => apply();
}
apply();
void load()
{
var palette = (Plugin.CurrentTrack as CoreAudioTrack)?._Palette ?? Palette.DEFAULT;
var palette = CurrentTrack?.Palette ?? Palette.DEFAULT;
var colors = palette.Colors;
var count = Math.Min(colors.Count(), maxCustomPaletteSize);
customPaletteSizeSyncedEntry.LocalValue = colors.Count();
customPaletteSizeEntry.Value = colors.Count();
for (int i = 0; i < maxCustomPaletteSize; i++)
{
var color = i < count ? colors[i] : Color.white;
string colorHex = $"#{ColorUtility.ToHtmlStringRGB(color)}";
customPaletteSyncedEntries[i].LocalValue = colorHex;
customPaletteEntries[i].Value = colorHex;
}
}
void apply()
{
int size = customPaletteSizeSyncedEntry.Value;
int size = customPaletteSizeEntry.Value;
if (size == 0 || size > maxCustomPaletteSize)
{
PaletteOverride = null;
}
else
{
var colors = customPaletteSyncedEntries.Select(entry => entry.Value).Take(size).ToArray();
var colors = customPaletteEntries.Select(entry => entry.Value).Take(size).ToArray();
PaletteOverride = Palette.Parse(colors);
}
}
@ -2080,94 +2056,90 @@ namespace MuzikaGromche
const string section = "Timings";
var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f);
// Declare and initialize early to avoid "Use of unassigned local variable"
List<(Action<CoreAudioTrack?> Load, Action Apply)> entries = [];
SyncedEntry<bool> overrideTimingsSyncedEntry = null!;
SyncedEntry<float> fadeOutBeatSyncedEntry = null!;
SyncedEntry<float> fadeOutDurationSyncedEntry = null!;
SyncedEntry<string> flickerLightsTimeSeriesSyncedEntry = null!;
SyncedEntry<string> lyricsTimeSeriesSyncedEntry = null!;
SyncedEntry<float> beatsOffsetSyncedEntry = null!;
SyncedEntry<float> colorTransitionInSyncedEntry = null!;
SyncedEntry<float> colorTransitionOutSyncedEntry = null!;
SyncedEntry<string> colorTransitionEasingSyncedEntry = null!;
List<(Action<IAudioTrack?> Load, Action Apply)> entries = [];
ConfigEntry<bool> overrideTimingsEntry = null!;
ConfigEntry<float> fadeOutBeatEntry = null!;
ConfigEntry<float> fadeOutDurationEntry = null!;
ConfigEntry<string> flickerLightsTimeSeriesEntry = null!;
ConfigEntry<string> lyricsTimeSeriesEntry = null!;
ConfigEntry<float> beatsOffsetEntry = null!;
ConfigEntry<float> colorTransitionInEntry = null!;
ConfigEntry<float> colorTransitionOutEntry = null!;
ConfigEntry<string> colorTransitionEasingEntry = null!;
var loadButton = new GenericButtonConfigItem(section, "Load Timings from the Current Track",
"Override custom timings with the built-in timings of the current track.", "Load", load);
loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost;
LethalConfigManager.AddConfigItem(loadButton);
overrideTimingsSyncedEntry = configFile.BindSyncedEntry(section, "Override Timings", false,
overrideTimingsEntry = configFile.Bind(section, "Override Timings", false,
new ConfigDescription("If checked, custom timings override track's own built-in timings."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(overrideTimingsSyncedEntry.Entry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(overrideTimingsSyncedEntry);
overrideTimingsSyncedEntry.Changed += (sender, args) => apply();
overrideTimingsSyncedEntry.SyncHostToLocal();
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(overrideTimingsEntry, Default(new BoolCheckBoxOptions())));
overrideTimingsEntry.SettingChanged += (sender, args) => apply();
fadeOutBeatSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Beat", 0f,
fadeOutBeatEntry = configFile.Bind(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,
fadeOutDurationEntry = configFile.Bind(section, "Fade Out Duration", 0f,
new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10)));
flickerLightsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Flicker Lights Time Series", "",
flickerLightsTimeSeriesEntry = configFile.Bind(section, "Flicker Lights Time Series", "",
new ConfigDescription("Time series of beat offsets when to flicker the lights."));
lyricsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Lyrics Time Series", "",
lyricsTimeSeriesEntry = configFile.Bind(section, "Lyrics Time Series", "",
new ConfigDescription("Time series of beat offsets when to show lyrics lines."));
beatsOffsetSyncedEntry = configFile.BindSyncedEntry(section, "Beats Offset", 0f,
beatsOffsetEntry = configFile.Bind(section, "Beats Offset", 0f,
new ConfigDescription("How much to offset the whole beat. More is later", new AcceptableValueRange<float>(-0.5f, 0.5f)));
colorTransitionInSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition In", 0.25f,
colorTransitionInEntry = configFile.Bind(section, "Color Transition In", 0.25f,
new ConfigDescription("Fraction of a beat *before* the whole beat when the color transition should start.", colorTransitionRange));
colorTransitionOutSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition Out", 0.25f,
colorTransitionOutEntry = configFile.Bind(section, "Color Transition Out", 0.25f,
new ConfigDescription("Fraction of a beat *after* the whole beat when the color transition should end.", colorTransitionRange));
colorTransitionEasingSyncedEntry = configFile.BindSyncedEntry(section, "Color Transition Easing", Easing.Linear.Name,
colorTransitionEasingEntry = configFile.Bind(section, "Color Transition Easing", Easing.Linear.Name,
new ConfigDescription("Interpolation/easing method to use for color transitions", new AcceptableValueList<string>(Easing.AllNames)));
var floatSliderOptions = Default(new FloatSliderOptions());
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutBeatSyncedEntry.Entry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationSyncedEntry.Entry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesSyncedEntry.Entry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesSyncedEntry.Entry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetSyncedEntry.Entry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInSyncedEntry.Entry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutSyncedEntry.Entry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextDropDownConfigItem(colorTransitionEasingSyncedEntry.Entry, Default(new TextDropDownOptions())));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutBeatEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextDropDownConfigItem(colorTransitionEasingEntry, Default(new TextDropDownOptions())));
registerStruct(fadeOutBeatSyncedEntry, t => t._FadeOutBeat, x => FadeOutBeatOverride = x);
registerStruct(fadeOutDurationSyncedEntry, t => t._FadeOutDuration, x => FadeOutDurationOverride = x);
registerArray(flickerLightsTimeSeriesSyncedEntry, t => t._FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true);
registerArray(lyricsTimeSeriesSyncedEntry, t => t._LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true);
registerStruct(beatsOffsetSyncedEntry, t => t._BeatsOffset, x => BeatsOffsetOverride = x);
registerStruct(colorTransitionInSyncedEntry, t => t._ColorTransitionIn, x => ColorTransitionInOverride = x);
registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = x);
registerClass(colorTransitionEasingSyncedEntry, t => t._ColorTransitionEasing.Name, x => ColorTransitionEasingOverride = x);
registerStruct(fadeOutBeatEntry, t => t.FadeOutBeat, x => FadeOutBeatOverride = x);
registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x);
registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true);
registerArray(lyricsTimeSeriesEntry, t => t.LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true);
registerStruct(beatsOffsetEntry, t => t.BeatsOffset, x => BeatsOffsetOverride = x);
registerStruct(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x);
registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x);
registerClass(colorTransitionEasingEntry, t => t.ColorTransitionEasing.Name, x => ColorTransitionEasingOverride = x);
void register<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action applier)
void register<T>(ConfigEntry<T> entry, Func<IAudioTrack, T> getter, Action applier)
{
CSyncHackAddSyncedEntry(syncedEntry);
syncedEntry.SyncHostToLocal();
syncedEntry.Changed += (sender, args) => applier();
void loader(CoreAudioTrack? track)
entry.SettingChanged += (sender, args) => applier();
void loader(IAudioTrack? track)
{
// if track is null, set everything to defaults
syncedEntry.LocalValue = track == null ? (T)syncedEntry.Entry.DefaultValue : getter(track);
entry.Value = track == null ? (T)entry.DefaultValue : getter(track);
}
entries.Add((loader, applier));
}
void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : struct =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null));
void registerClass<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : class =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null));
void registerArray<T>(SyncedEntry<string> syncedEntry, Func<CoreAudioTrack, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct =>
register(syncedEntry,
void registerStruct<T>(ConfigEntry<T> entry, Func<IAudioTrack, T> getter, Action<T?> setter) where T : struct =>
register(entry, getter, () => setter.Invoke(overrideTimingsEntry.Value ? entry.Value : null));
void registerClass<T>(ConfigEntry<T> entry, Func<IAudioTrack, T> getter, Action<T?> setter) where T : class =>
register(entry, getter, () => setter.Invoke(overrideTimingsEntry.Value ? entry.Value : null));
void registerArray<T>(ConfigEntry<string> entry, Func<IAudioTrack, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct =>
register(entry,
(track) => string.Join(", ", getter(track)),
() =>
{
var values = parseStringArray(syncedEntry.Value, parser, sort);
var values = parseStringArray(entry.Value, parser, sort);
if (values != null)
{
// ensure the entry is sorted and formatted
syncedEntry.LocalValue = string.Join(", ", values);
entry.Value = string.Join(", ", values);
}
setter.Invoke(overrideTimingsSyncedEntry.Value ? values : null);
setter.Invoke(overrideTimingsEntry.Value ? values : null);
});
T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct
@ -2187,10 +2159,9 @@ namespace MuzikaGromche
void load()
{
var track = Plugin.CurrentTrack;
foreach (var entry in entries)
{
entry.Load(track as CoreAudioTrack);
entry.Load(CurrentTrack);
}
}
@ -2229,19 +2200,59 @@ namespace MuzikaGromche
}
else
{
Debug.Log($"{nameof(MuzikaGromche)} Patching {nameof(JesterAI)} with {nameof(MuzikaGromcheJesterNetworkBehaviour)} component");
networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>();
Debug.Log($"{nameof(MuzikaGromche)} Patched JesterEnemy");
}
}
}
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
{
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
const string LoopAudioGameObjectName = "MuzikaGromcheAudio (Loop)";
// Number of times a selected track has been played.
// Increases by 1 with each ChooseTrackServerRpc call.
// Resets on SettingChanged.
private int SelectedTrackIndex = 0;
internal IAudioTrack? CurrentTrack = null;
internal BeatTimeState? BeatTimeState = null;
internal AudioSource IntroAudioSource = null!;
internal AudioSource LoopAudioSource = null!;
void Awake()
{
var farAudioTransform = gameObject.transform.Find("FarAudio");
if (farAudioTransform == null)
{
Debug.LogError($"{nameof(MuzikaGromche)} JesterEnemy->FarAudio prefab not found!");
}
else
{
// Instead of hijacking farAudio and creatureVoice sources,
// create our own copies to ensure uniform playback experience.
// For reasons unknown adding them directly to the prefab didn't work.
var introAudioGameObject = Instantiate(farAudioTransform.gameObject, gameObject.transform);
introAudioGameObject.name = IntroAudioGameObjectName;
var loopAudioGameObject = Instantiate(farAudioTransform.gameObject, gameObject.transform);
loopAudioGameObject.name = LoopAudioGameObjectName;
IntroAudioSource = introAudioGameObject.GetComponent<AudioSource>();
IntroAudioSource.maxDistance = Plugin.AudioMaxDistance;
IntroAudioSource.dopplerLevel = 0;
IntroAudioSource.loop = false;
LoopAudioSource = loopAudioGameObject.GetComponent<AudioSource>();
LoopAudioSource.maxDistance = Plugin.AudioMaxDistance;
LoopAudioSource.dopplerLevel = 0;
LoopAudioSource.loop = true;
Debug.Log($"{nameof(MuzikaGromche)} {nameof(MuzikaGromcheJesterNetworkBehaviour)} Patched JesterEnemy");
}
}
public override void OnNetworkSpawn()
{
ChooseTrackDeferred();
@ -2293,7 +2304,10 @@ namespace MuzikaGromche
public void SetTrackClientRpc(string name)
{
Debug.Log($"{nameof(MuzikaGromche)} SetTrackClientRpc {name}");
Plugin.CurrentTrack = Plugin.FindTrackNamed(name);
if (Plugin.FindTrackNamed(name) is { } track)
{
CurrentTrack = Config.OverrideCurrentTrack(track);
}
}
[ServerRpc]
@ -2305,26 +2319,37 @@ namespace MuzikaGromche
SetTrackClientRpc(audioTrack.Name);
SelectedTrackIndex += 1;
}
internal void PlayScheduledLoop()
{
double loopStartDspTime = AudioSettings.dspTime + IntroAudioSource.clip.length - IntroAudioSource.time;
LoopAudioSource.PlayScheduled(loopStartDspTime);
Debug.Log($"{nameof(MuzikaGromche)} Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}");
}
}
// farAudio is during windup, Intro overrides popGoesTheWeaselTheme
// creatureVoice is when popped, Loop overrides screamingSFX
[HarmonyPatch(typeof(JesterAI))]
static class JesterPatch
{
#if DEBUG
[HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))]
[HarmonyPostfix]
static void AlmostInstantFollowTimerPostfix(JesterAI __instance)
static void SetJesterInitialValuesPostfix(JesterAI __instance)
{
var behaviour = __instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>();
behaviour.IntroAudioSource.Stop();
behaviour.LoopAudioSource.Stop();
#if DEBUG
// Almost instant follow timer
__instance.beginCrankingTimer = 1f;
}
#endif
}
class State
{
public required AudioSource farAudio;
public required int previousState;
public int currentBehaviourStateIndex;
public int previousState;
public float stunNormalizedTimer;
}
[HarmonyPatch(nameof(JesterAI.Update))]
@ -2333,96 +2358,97 @@ namespace MuzikaGromche
{
__state = new State
{
farAudio = __instance.farAudio,
currentBehaviourStateIndex = __instance.currentBehaviourStateIndex,
previousState = __instance.previousState,
stunNormalizedTimer = __instance.stunNormalizedTimer,
};
if (__instance.currentBehaviourStateIndex == 2 && __instance.previousState != 2)
{
// If just popped out, then override farAudio so that vanilla logic does not stop the modded Intro music.
// The game will stop farAudio it during its Update, so we temporarily set it to any other AudioSource
// which we don't care about stopping for now.
//
// Why creatureVoice though? We gonna need creatureVoice later in Postfix to schedule the Loop,
// but right now we still don't care if it's stopped, so it shouldn't matter.
// And it's cheaper and simpler than figuring out how to instantiate an AudioSource behaviour.
__instance.farAudio = __instance.creatureVoice;
}
}
[HarmonyPatch(nameof(JesterAI.Update))]
[HarmonyPostfix]
static void JesterUpdatePostfix(JesterAI __instance, State __state)
{
if (Plugin.CurrentTrack == null)
var behaviour = __instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>();
var introAudioSource = behaviour.IntroAudioSource;
var loopAudioSource = behaviour.LoopAudioSource;
if (behaviour.CurrentTrack == null)
{
#if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
Debug.LogError($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
#endif
return;
}
if (__instance.previousState == 1 && __state.previousState != 1)
// This switch statement resembles the one from JesterAI.Update
switch (__state.currentBehaviourStateIndex)
{
case 1:
if (__state.previousState != 1)
{
// if just started winding up
// then stop the default music...
__instance.farAudio.Stop();
__instance.creatureVoice.Stop();
// then stop the default music... (already done above)
// ...and set up both modded audio clips in advance
introAudioSource.clip = behaviour.CurrentTrack.LoadedIntro;
loopAudioSource.clip = behaviour.CurrentTrack.LoadedLoop;
behaviour.BeatTimeState = new BeatTimeState(behaviour.CurrentTrack);
// ...and start modded music
Plugin.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack);
// Set up custom popup timer, which is shorter than Start audio
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
// Set up custom popup timer, which is shorter than Intro audio
__instance.popUpTimer = behaviour.CurrentTrack.WindUpTimer;
// Override popGoesTheWeaselTheme with Start audio
__instance.farAudio.maxDistance = Plugin.AudioMaxDistance;
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro;
__instance.farAudio.loop = false;
if (Config.ShouldSkipWindingPhase)
{
var rewind = 5f;
__instance.popUpTimer = rewind;
__instance.farAudio.time = Plugin.CurrentTrack.WindUpTimer - rewind;
introAudioSource.time = behaviour.CurrentTrack.WindUpTimer - rewind;
}
else
{
// reset if previously skipped winding by assigning different starting time.
__instance.farAudio.time = 0;
}
__instance.farAudio.Play();
Debug.Log($"{nameof(MuzikaGromche)} Playing Intro music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}");
introAudioSource.time = 0f;
}
if (__instance.previousState != 2 && __state.previousState == 2)
__instance.farAudio.Stop();
introAudioSource.Play();
behaviour.PlayScheduledLoop();
}
if (__instance.stunNormalizedTimer > 0f)
{
introAudioSource.Pause();
loopAudioSource.Stop();
}
else
{
if (!introAudioSource.isPlaying)
{
__instance.farAudio.Stop();
introAudioSource.UnPause();
behaviour.PlayScheduledLoop();
}
}
break;
case 2:
if (__state.previousState != 2)
{
__instance.creatureVoice.Stop();
}
break;
}
// transition away from state 2 ("poppedOut"), normally to state 0
if (__state.previousState == 2 && __instance.previousState != 2)
{
Plugin.ResetLightColor();
DiscoBallManager.Disable();
// Rotate track groups
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc();
}
if (__instance.previousState == 2 && __state.previousState != 2)
{
// Restore stashed AudioSource. See the comment in Prefix
__instance.farAudio = __state.farAudio;
var time = __instance.farAudio.time;
var delay = Plugin.CurrentTrack.LoadedIntro.length - time;
// Override screamingSFX with Loop, delayed by the remaining time of the Intro audio
__instance.creatureVoice.Stop();
__instance.creatureVoice.maxDistance = Plugin.AudioMaxDistance;
__instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop;
__instance.creatureVoice.PlayDelayed(delay);
Debug.Log($"{nameof(MuzikaGromche)} Intro length: {Plugin.CurrentTrack.LoadedIntro.length}; played time: {time}");
Debug.Log($"{nameof(MuzikaGromche)} Playing loop music: maxDistance: {__instance.creatureVoice.maxDistance}, minDistance: {__instance.creatureVoice.minDistance}, volume: {__instance.creatureVoice.volume}, spread: {__instance.creatureVoice.spread}, in seconds: {delay}");
behaviour.ChooseTrackServerRpc();
behaviour.BeatTimeState = null;
}
// 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)
else if ((__instance.previousState == 1 || __instance.previousState == 2) && behaviour.BeatTimeState is { } beatTimeState)
{
var events = beatTimeState.Update(intro: __instance.farAudio, loop: __instance.creatureVoice);
var events = beatTimeState.Update(introAudioSource, loopAudioSource);
foreach (var ev in events)
{
switch (ev)
@ -2465,8 +2491,8 @@ namespace MuzikaGromche
Plugin.ResetLightColor();
DiscoBallManager.Disable();
// Just in case if players have spawned multiple Jesters,
// Don't reset Plugin.CurrentTrack and Plugin.BeatTimeState to null,
// so that the code wouldn't crash without extra null checks.
// Don't reset Plugin.CurrentTrack to null,
// so that the latest chosen track remains set.
}
}
}

View File

@ -252,6 +252,7 @@ namespace MuzikaGromche
static bool OnRefreshLightsList(RoundManager __instance)
{
RefreshLightsListPatched(__instance);
LoadInitialLightsColors(__instance);
// Skip the original method
return false;
}
@ -286,5 +287,15 @@ namespace MuzikaGromche
animator.SetFloat("flickerSpeed", UnityEngine.Random.Range(0.6f, 1.4f));
}
}
static void LoadInitialLightsColors(RoundManager self)
{
var originalColors = new Dictionary<Light, Color>();
foreach (var light in self.allPoweredLights)
{
originalColors[light] = light.color;
}
Plugin.InitialLightsColors = originalColors;
}
}
}

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 ([`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 are working. [`LobbyCompatibility`] is recommended but optional.
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.
@ -36,7 +36,6 @@ Any player can change their personal preferences locally.
1. Actually not limited to Inverse teleporter or Titan.
[`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,13 +1,12 @@
{
"name": "MuzikaGromche",
"version_number": "1337.420.9001",
"version_number": "1337.420.9002",
"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",
"AinaVT-LethalConfig-1.4.6",
"WaterGun-V70PoweredLights_Fix-1.0.0",
"BMX-LobbyCompatibility-1.5.1"
"WaterGun-V70PoweredLights_Fix-1.0.0"
]
}