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 # 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 ## MuzikaGromche 1337.420.9001 - Multiverse Edition
- Added support for tracks to rotate between multiple audio variants during a round. - 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> <AssemblyName>Ratijas.MuzikaGromche</AssemblyName>
<Product>Muzika Gromche</Product> <Product>Muzika Gromche</Product>
<Description>Add some content to your inverse teleporter experience on Titan!</Description> <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> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -38,12 +38,6 @@
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" PrivateAssets="all" Private="false" /> <PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="UnityEngine.Modules" Version="2022.3.9" 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" /> <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="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" /> <PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
</ItemGroup> </ItemGroup>

View File

@ -4,8 +4,6 @@ using HarmonyLib;
using LethalConfig; using LethalConfig;
using LethalConfig.ConfigItems; using LethalConfig.ConfigItems;
using LethalConfig.ConfigItems.Options; using LethalConfig.ConfigItems.Options;
using LobbyCompatibility.Attributes;
using LobbyCompatibility.Enums;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@ -19,25 +17,20 @@ using Unity.Netcode;
using UnityEngine; using UnityEngine;
using UnityEngine.Networking; using UnityEngine.Networking;
#if DEBUG
using CSync.Extensions;
using CSync.Lib;
#endif
namespace MuzikaGromche namespace MuzikaGromche
{ {
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] [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("ainavt.lc.lethalconfig", "1.4.6")]
[BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)]
[BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.HardDependency)] [BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.SoftDependency)]
[LobbyCompatibility(CompatibilityLevel.Everyone, VersionStrictness.Patch)]
public class Plugin : BaseUnityPlugin public class Plugin : BaseUnityPlugin
{ {
internal new static Config Config { get; private set; } = null!; 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 = [ private static readonly string[] PwnLyricsVariants = [
"", "", "", // make sure the array has enough items to index it without checking "", "", "", // make sure the array has enough items to index it without checking
..NetworkInterface.GetAllNetworkInterfaces() ..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() public static ISelectableTrack ChooseTrack()
@ -584,9 +597,6 @@ namespace MuzikaGromche
return Tracks.SelectMany(track => track.GetTracks()).FirstOrDefault(track => track.Name == name); 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) public static void SetLightColor(Color color)
{ {
foreach (var light in RoundManager.Instance.allPoweredLights) foreach (var light in RoundManager.Instance.allPoweredLights)
@ -597,7 +607,10 @@ namespace MuzikaGromche
public static void ResetLightColor() public static void ResetLightColor()
{ {
SetLightColor(Color.white); foreach (var (light, color) in InitialLightsColors)
{
light.color = color;
}
} }
// Max audible distance for AudioSource and LyricsEvent // Max audible distance for AudioSource and LyricsEvent
@ -684,6 +697,7 @@ namespace MuzikaGromche
harmony.PatchAll(typeof(DiscoBallDespawnPatch)); harmony.PatchAll(typeof(DiscoBallDespawnPatch));
harmony.PatchAll(typeof(SpawnRatePatch)); harmony.PatchAll(typeof(SpawnRatePatch));
NetcodePatcher(); NetcodePatcher();
Compatibility.Register(this);
} }
else else
{ {
@ -715,6 +729,7 @@ namespace MuzikaGromche
public static readonly Language ENGLISH = new("EN", "English"); public static readonly Language ENGLISH = new("EN", "English");
public static readonly Language RUSSIAN = new("RU", "Russian"); public static readonly Language RUSSIAN = new("RU", "Russian");
public static readonly Language KOREAN = new("KO", "Korean"); public static readonly Language KOREAN = new("KO", "Korean");
public static readonly Language JAPANESE = new("JP", "Japanese");
public static readonly Language HINDI = new("HI", "Hindi"); 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 // An instance of a track which appears as a configuration entry and
// can be selected using weighted random from a list of selectable tracks. // can be selected using weighted random from a list of selectable tracks.
public interface ISelectableTrack public interface ISelectableTrack
@ -899,13 +899,38 @@ namespace MuzikaGromche
public Palette Palette { get; } 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. // Core audio track implementation with some defaults and config overrides.
// Suitable to declare elemnents of SelectableTracksGroup and as a base for standalone selectable tracks. // Suitable to declare elemnents of SelectableTracksGroup and as a base for standalone selectable tracks.
public class CoreAudioTrack : IAudioTrack public class CoreAudioTrack : IAudioTrack
{ {
public required string Name { get; init; } public /* required */ string Name { get; init; } = "";
public required float WindUpTimer { get; init; } public /* required */ float WindUpTimer { get; init; } = 0f;
public int Beats { get; init; } public int Beats { get; init; }
// Shorthand for four beats // Shorthand for four beats
@ -933,56 +958,17 @@ namespace MuzikaGromche
init => FileNameLoopOverride = value; init => FileNameLoopOverride = value;
} }
public float _BeatsOffset = 0f; public float BeatsOffset { get; init; } = 0f;
public float BeatsOffset public float FadeOutBeat { get; init; } = float.NaN;
{ public float FadeOutDuration { get; init; } = 2f;
get => Config.BeatsOffsetOverride ?? _BeatsOffset; public float ColorTransitionIn { get; init; } = 0.25f;
init => _BeatsOffset = value; public float ColorTransitionOut { get; init; } = 0.25f;
} public Easing ColorTransitionEasing { get; init; } = Easing.OutExpo;
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[] _FlickerLightsTimeSeries = []; public float[] _FlickerLightsTimeSeries = [];
public float[] FlickerLightsTimeSeries public float[] FlickerLightsTimeSeries
{ {
get => Config.FlickerLightsTimeSeriesOverride ?? _FlickerLightsTimeSeries; get => _FlickerLightsTimeSeries;
init init
{ {
Array.Sort(value); Array.Sort(value);
@ -990,12 +976,7 @@ namespace MuzikaGromche
} }
} }
public float[] _LyricsTimeSeries = []; public float[] LyricsTimeSeries { get; private set; } = [];
public float[] LyricsTimeSeries
{
get => Config.LyricsTimeSeriesOverride ?? _LyricsTimeSeries;
private set => _LyricsTimeSeries = value;
}
// Lyrics line may contain multiple tab-separated alternatives. // Lyrics line may contain multiple tab-separated alternatives.
// In such case, a random number chosen and updated once per loop // 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; set; } = Palette.DEFAULT;
public Palette Palette
{
get => Config.PaletteOverride ?? _Palette;
set => _Palette = value;
}
} }
// Standalone, top-level, selectable audio track // Standalone, top-level, selectable audio track
public class SelectableAudioTrack : CoreAudioTrack, ISelectableTrack public class SelectableAudioTrack : CoreAudioTrack, ISelectableTrack
{ {
public required Language Language { get; init; } public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false; public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!; ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
@ -1043,12 +1019,12 @@ namespace MuzikaGromche
public class SelectableTracksGroup : ISelectableTrack public class SelectableTracksGroup : ISelectableTrack
{ {
public required string Name { get; init; } public /* required */ string Name { get; init; } = "";
public required Language Language { get; init; } public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false; public bool IsExplicit { get; init; } = false;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!; ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
public required IAudioTrack[] Tracks; public /* required */ IAudioTrack[] Tracks = [];
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks; IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
@ -1583,7 +1559,9 @@ namespace MuzikaGromche
if (windUpOffsetTimestamp.Beat < 0f && track.FadeOutBeat < loopOffsetSpan.BeatToInclusive && loopOffsetSpan.BeatFromExclusive <= fadeOutEnd) if (windUpOffsetTimestamp.Beat < 0f && track.FadeOutBeat < loopOffsetSpan.BeatToInclusive && loopOffsetSpan.BeatFromExclusive <= fadeOutEnd)
{ {
var t = (loopOffsetSpan.BeatToInclusive - track.FadeOutBeat) / track.FadeOutDuration; 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 else
{ {
@ -1656,7 +1634,9 @@ namespace MuzikaGromche
} }
else 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; } 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 class Config
#if DEBUG
: SyncedConfig2<Config>
#endif
{ {
public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!; 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 ExtrapolateTime { get; private set; } = true;
public static bool ShouldSkipWindingPhase { get; private set; } = false; 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; private class AudioTrackWithConfigOverride(IAudioTrack track) : ProxyAudioTrack(track), IAudioTrack
public static float? FadeOutDurationOverride { get; private set; } = null; {
public static float[]? FlickerLightsTimeSeriesOverride { get; private set; } = null; float IAudioTrack.BeatsOffset => BeatsOffsetOverride ?? Track.BeatsOffset;
public static float[]? LyricsTimeSeriesOverride { get; private set; } = null;
public static float? BeatsOffsetOverride { get; private set; } = null; float IAudioTrack.FadeOutBeat => FadeOutBeatOverride ?? Track.FadeOutBeat;
public static float? ColorTransitionInOverride { get; private set; } = null;
public static float? ColorTransitionOutOverride { get; private set; } = null; float IAudioTrack.FadeOutDuration => FadeOutDurationOverride ?? Track.FadeOutDuration;
public static string? ColorTransitionEasingOverride { get; private set; } = null;
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) internal Config(ConfigFile configFile)
#if DEBUG
: base(PluginInfo.PLUGIN_GUID)
#endif
{ {
DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, DisplayLyrics = configFile.Bind("General", "Display Lyrics", true,
new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music.")); 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.")); new ConfigDescription("When choosing tracks at random, skip the ones with Explicit Content/Lyrics."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(SkipExplicitTracks, Default(new BoolCheckBoxOptions()))); 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.")); new ConfigDescription("Deviate from vanilla spawn rates to experience content of this mod more often."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions())));
@ -1928,21 +1916,17 @@ namespace MuzikaGromche
LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight, Default(new IntSliderOptions()))); LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight, Default(new IntSliderOptions())));
} }
#if DEBUG
ConfigManager.Register(this);
#endif
} }
#if DEBUG internal static IAudioTrack OverrideCurrentTrack(IAudioTrack track)
// 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)
{ {
// This is basically what ConfigFile.PopulateEntryContainer does #if DEBUG
EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase); CurrentTrack = track;
} return new AudioTrackWithConfigOverride(track);
#else
return track;
#endif #endif
}
public static CanModifyResult CanModifyIfHost() public static CanModifyResult CanModifyIfHost()
{ {
@ -1981,33 +1965,29 @@ namespace MuzikaGromche
#if DEBUG #if DEBUG
private void SetupEntriesForExtrapolation(ConfigFile configFile) 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.")); 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()))); LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(syncedEntry); entry.SettingChanged += (sender, args) => apply();
syncedEntry.Changed += (sender, args) => apply();
syncedEntry.SyncHostToLocal();
apply(); apply();
void apply() void apply()
{ {
ExtrapolateTime = syncedEntry.Value; ExtrapolateTime = entry.Value;
} }
} }
private void SetupEntriesToSkipWinding(ConfigFile configFile) 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.")); 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()))); LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(entry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(syncedEntry); entry.SettingChanged += (sender, args) => apply();
syncedEntry.Changed += (sender, args) => apply();
syncedEntry.SyncHostToLocal();
apply(); apply();
void apply() void apply()
{ {
ShouldSkipWindingPhase = syncedEntry.Value; ShouldSkipWindingPhase = entry.Value;
} }
} }
@ -2016,60 +1996,56 @@ namespace MuzikaGromche
const string section = "Palette"; const string section = "Palette";
const int maxCustomPaletteSize = 8; const int maxCustomPaletteSize = 8;
// Declare and initialize early to avoid "Use of unassigned local variable" // Declare and initialize early to avoid "Use of unassigned local variable"
SyncedEntry<int> customPaletteSizeSyncedEntry = null!; ConfigEntry<int> customPaletteSizeEntry = null!;
var customPaletteSyncedEntries = new SyncedEntry<string>[maxCustomPaletteSize]; var customPaletteEntries = new ConfigEntry<string>[maxCustomPaletteSize];
var loadButton = new GenericButtonConfigItem(section, "Load Palette from the Current Track", 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); "Override custom palette with the built-in palette of the current track.", "Load", load);
loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost; loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost;
LethalConfigManager.AddConfigItem(loadButton); 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.", "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))); new AcceptableValueRange<int>(0, maxCustomPaletteSize)));
LethalConfigManager.AddConfigItem(new IntSliderConfigItem(customPaletteSizeSyncedEntry.Entry, Default(new IntSliderOptions()))); LethalConfigManager.AddConfigItem(new IntSliderConfigItem(customPaletteSizeEntry, Default(new IntSliderOptions())));
CSyncHackAddSyncedEntry(customPaletteSizeSyncedEntry); customPaletteSizeEntry.SettingChanged += (sender, args) => apply();
customPaletteSizeSyncedEntry.Changed += (sender, args) => apply();
customPaletteSizeSyncedEntry.SyncHostToLocal();
for (int i = 0; i < maxCustomPaletteSize; i++) for (int i = 0; i < maxCustomPaletteSize; i++)
{ {
string entryName = $"Custom Color {i + 1}"; string entryName = $"Custom Color {i + 1}";
var customColorSyncedEntry = configFile.BindSyncedEntry(section, entryName, "#FFFFFF", "Choose color for the custom palette"); var customColorEntry = configFile.Bind(section, entryName, "#FFFFFF", "Choose color for the custom palette");
customPaletteSyncedEntries[i] = customColorSyncedEntry; customPaletteEntries[i] = customColorEntry;
LethalConfigManager.AddConfigItem(new HexColorInputFieldConfigItem(customColorSyncedEntry.Entry, Default(new HexColorInputFieldOptions()))); LethalConfigManager.AddConfigItem(new HexColorInputFieldConfigItem(customColorEntry, Default(new HexColorInputFieldOptions())));
CSyncHackAddSyncedEntry(customColorSyncedEntry); customColorEntry.SettingChanged += (sender, args) => apply();
customColorSyncedEntry.Changed += (sender, args) => apply();
customColorSyncedEntry.SyncHostToLocal();
} }
apply(); apply();
void load() void load()
{ {
var palette = (Plugin.CurrentTrack as CoreAudioTrack)?._Palette ?? Palette.DEFAULT; var palette = CurrentTrack?.Palette ?? Palette.DEFAULT;
var colors = palette.Colors; var colors = palette.Colors;
var count = Math.Min(colors.Count(), maxCustomPaletteSize); var count = Math.Min(colors.Count(), maxCustomPaletteSize);
customPaletteSizeSyncedEntry.LocalValue = colors.Count(); customPaletteSizeEntry.Value = colors.Count();
for (int i = 0; i < maxCustomPaletteSize; i++) for (int i = 0; i < maxCustomPaletteSize; i++)
{ {
var color = i < count ? colors[i] : Color.white; var color = i < count ? colors[i] : Color.white;
string colorHex = $"#{ColorUtility.ToHtmlStringRGB(color)}"; string colorHex = $"#{ColorUtility.ToHtmlStringRGB(color)}";
customPaletteSyncedEntries[i].LocalValue = colorHex; customPaletteEntries[i].Value = colorHex;
} }
} }
void apply() void apply()
{ {
int size = customPaletteSizeSyncedEntry.Value; int size = customPaletteSizeEntry.Value;
if (size == 0 || size > maxCustomPaletteSize) if (size == 0 || size > maxCustomPaletteSize)
{ {
PaletteOverride = null; PaletteOverride = null;
} }
else 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); PaletteOverride = Palette.Parse(colors);
} }
} }
@ -2080,94 +2056,90 @@ namespace MuzikaGromche
const string section = "Timings"; const string section = "Timings";
var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f); var colorTransitionRange = new AcceptableValueRange<float>(0f, 1f);
// Declare and initialize early to avoid "Use of unassigned local variable" // Declare and initialize early to avoid "Use of unassigned local variable"
List<(Action<CoreAudioTrack?> Load, Action Apply)> entries = []; List<(Action<IAudioTrack?> Load, Action Apply)> entries = [];
SyncedEntry<bool> overrideTimingsSyncedEntry = null!; ConfigEntry<bool> overrideTimingsEntry = null!;
SyncedEntry<float> fadeOutBeatSyncedEntry = null!; ConfigEntry<float> fadeOutBeatEntry = null!;
SyncedEntry<float> fadeOutDurationSyncedEntry = null!; ConfigEntry<float> fadeOutDurationEntry = null!;
SyncedEntry<string> flickerLightsTimeSeriesSyncedEntry = null!; ConfigEntry<string> flickerLightsTimeSeriesEntry = null!;
SyncedEntry<string> lyricsTimeSeriesSyncedEntry = null!; ConfigEntry<string> lyricsTimeSeriesEntry = null!;
SyncedEntry<float> beatsOffsetSyncedEntry = null!; ConfigEntry<float> beatsOffsetEntry = null!;
SyncedEntry<float> colorTransitionInSyncedEntry = null!; ConfigEntry<float> colorTransitionInEntry = null!;
SyncedEntry<float> colorTransitionOutSyncedEntry = null!; ConfigEntry<float> colorTransitionOutEntry = null!;
SyncedEntry<string> colorTransitionEasingSyncedEntry = null!; ConfigEntry<string> colorTransitionEasingEntry = null!;
var loadButton = new GenericButtonConfigItem(section, "Load Timings from the Current Track", 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); "Override custom timings with the built-in timings of the current track.", "Load", load);
loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost; loadButton.ButtonOptions.CanModifyCallback = CanModifyIfHost;
LethalConfigManager.AddConfigItem(loadButton); 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.")); new ConfigDescription("If checked, custom timings override track's own built-in timings."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(overrideTimingsSyncedEntry.Entry, Default(new BoolCheckBoxOptions()))); LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(overrideTimingsEntry, Default(new BoolCheckBoxOptions())));
CSyncHackAddSyncedEntry(overrideTimingsSyncedEntry); overrideTimingsEntry.SettingChanged += (sender, args) => apply();
overrideTimingsSyncedEntry.Changed += (sender, args) => apply();
overrideTimingsSyncedEntry.SyncHostToLocal();
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))); 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))); 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.")); 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.")); 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))); 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)); 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)); 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))); new ConfigDescription("Interpolation/easing method to use for color transitions", new AcceptableValueList<string>(Easing.AllNames)));
var floatSliderOptions = Default(new FloatSliderOptions()); var floatSliderOptions = Default(new FloatSliderOptions());
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutBeatSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutBeatEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(fadeOutDurationEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesSyncedEntry.Entry, Default(new TextInputFieldOptions()))); LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(flickerLightsTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesSyncedEntry.Entry, Default(new TextInputFieldOptions()))); LethalConfigManager.AddConfigItem(new TextInputFieldConfigItem(lyricsTimeSeriesEntry, Default(new TextInputFieldOptions())));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(beatsOffsetEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionInEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutSyncedEntry.Entry, floatSliderOptions)); LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(colorTransitionOutEntry, floatSliderOptions));
LethalConfigManager.AddConfigItem(new TextDropDownConfigItem(colorTransitionEasingSyncedEntry.Entry, Default(new TextDropDownOptions()))); LethalConfigManager.AddConfigItem(new TextDropDownConfigItem(colorTransitionEasingEntry, Default(new TextDropDownOptions())));
registerStruct(fadeOutBeatSyncedEntry, t => t._FadeOutBeat, x => FadeOutBeatOverride = x); registerStruct(fadeOutBeatEntry, t => t.FadeOutBeat, x => FadeOutBeatOverride = x);
registerStruct(fadeOutDurationSyncedEntry, t => t._FadeOutDuration, x => FadeOutDurationOverride = x); registerStruct(fadeOutDurationEntry, t => t.FadeOutDuration, x => FadeOutDurationOverride = x);
registerArray(flickerLightsTimeSeriesSyncedEntry, t => t._FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true); registerArray(flickerLightsTimeSeriesEntry, t => t.FlickerLightsTimeSeries, xs => FlickerLightsTimeSeriesOverride = xs, float.Parse, sort: true);
registerArray(lyricsTimeSeriesSyncedEntry, t => t._LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true); registerArray(lyricsTimeSeriesEntry, t => t.LyricsTimeSeries, xs => LyricsTimeSeriesOverride = xs, float.Parse, sort: true);
registerStruct(beatsOffsetSyncedEntry, t => t._BeatsOffset, x => BeatsOffsetOverride = x); registerStruct(beatsOffsetEntry, t => t.BeatsOffset, x => BeatsOffsetOverride = x);
registerStruct(colorTransitionInSyncedEntry, t => t._ColorTransitionIn, x => ColorTransitionInOverride = x); registerStruct(colorTransitionInEntry, t => t.ColorTransitionIn, x => ColorTransitionInOverride = x);
registerStruct(colorTransitionOutSyncedEntry, t => t._ColorTransitionOut, x => ColorTransitionOutOverride = x); registerStruct(colorTransitionOutEntry, t => t.ColorTransitionOut, x => ColorTransitionOutOverride = x);
registerClass(colorTransitionEasingSyncedEntry, t => t._ColorTransitionEasing.Name, x => ColorTransitionEasingOverride = 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); entry.SettingChanged += (sender, args) => applier();
syncedEntry.SyncHostToLocal(); void loader(IAudioTrack? track)
syncedEntry.Changed += (sender, args) => applier();
void loader(CoreAudioTrack? track)
{ {
// if track is null, set everything to defaults // 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)); entries.Add((loader, applier));
} }
void registerStruct<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : struct => void registerStruct<T>(ConfigEntry<T> entry, Func<IAudioTrack, T> getter, Action<T?> setter) where T : struct =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null)); register(entry, getter, () => setter.Invoke(overrideTimingsEntry.Value ? entry.Value : null));
void registerClass<T>(SyncedEntry<T> syncedEntry, Func<CoreAudioTrack, T> getter, Action<T?> setter) where T : class => void registerClass<T>(ConfigEntry<T> entry, Func<IAudioTrack, T> getter, Action<T?> setter) where T : class =>
register(syncedEntry, getter, () => setter.Invoke(overrideTimingsSyncedEntry.Value ? syncedEntry.Value : null)); register(entry, getter, () => setter.Invoke(overrideTimingsEntry.Value ? entry.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 => void registerArray<T>(ConfigEntry<string> entry, Func<IAudioTrack, T[]> getter, Action<T[]?> setter, Func<string, T> parser, bool sort = false) where T : struct =>
register(syncedEntry, register(entry,
(track) => string.Join(", ", getter(track)), (track) => string.Join(", ", getter(track)),
() => () =>
{ {
var values = parseStringArray(syncedEntry.Value, parser, sort); var values = parseStringArray(entry.Value, parser, sort);
if (values != null) if (values != null)
{ {
// ensure the entry is sorted and formatted // 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 T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct
@ -2187,10 +2159,9 @@ namespace MuzikaGromche
void load() void load()
{ {
var track = Plugin.CurrentTrack;
foreach (var entry in entries) foreach (var entry in entries)
{ {
entry.Load(track as CoreAudioTrack); entry.Load(CurrentTrack);
} }
} }
@ -2229,19 +2200,59 @@ namespace MuzikaGromche
} }
else else
{ {
Debug.Log($"{nameof(MuzikaGromche)} Patching {nameof(JesterAI)} with {nameof(MuzikaGromcheJesterNetworkBehaviour)} component");
networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>(); networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>();
Debug.Log($"{nameof(MuzikaGromche)} Patched JesterEnemy");
} }
} }
} }
class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour class MuzikaGromcheJesterNetworkBehaviour : NetworkBehaviour
{ {
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
const string LoopAudioGameObjectName = "MuzikaGromcheAudio (Loop)";
// Number of times a selected track has been played. // Number of times a selected track has been played.
// Increases by 1 with each ChooseTrackServerRpc call. // Increases by 1 with each ChooseTrackServerRpc call.
// Resets on SettingChanged. // Resets on SettingChanged.
private int SelectedTrackIndex = 0; 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() public override void OnNetworkSpawn()
{ {
ChooseTrackDeferred(); ChooseTrackDeferred();
@ -2293,7 +2304,10 @@ namespace MuzikaGromche
public void SetTrackClientRpc(string name) public void SetTrackClientRpc(string name)
{ {
Debug.Log($"{nameof(MuzikaGromche)} SetTrackClientRpc {name}"); Debug.Log($"{nameof(MuzikaGromche)} SetTrackClientRpc {name}");
Plugin.CurrentTrack = Plugin.FindTrackNamed(name); if (Plugin.FindTrackNamed(name) is { } track)
{
CurrentTrack = Config.OverrideCurrentTrack(track);
}
} }
[ServerRpc] [ServerRpc]
@ -2305,26 +2319,37 @@ namespace MuzikaGromche
SetTrackClientRpc(audioTrack.Name); SetTrackClientRpc(audioTrack.Name);
SelectedTrackIndex += 1; 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))] [HarmonyPatch(typeof(JesterAI))]
static class JesterPatch static class JesterPatch
{ {
#if DEBUG
[HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))] [HarmonyPatch(nameof(JesterAI.SetJesterInitialValues))]
[HarmonyPostfix] [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; __instance.beginCrankingTimer = 1f;
}
#endif #endif
}
class State class State
{ {
public required AudioSource farAudio; public int currentBehaviourStateIndex;
public required int previousState; public int previousState;
public float stunNormalizedTimer;
} }
[HarmonyPatch(nameof(JesterAI.Update))] [HarmonyPatch(nameof(JesterAI.Update))]
@ -2333,96 +2358,97 @@ namespace MuzikaGromche
{ {
__state = new State __state = new State
{ {
farAudio = __instance.farAudio, currentBehaviourStateIndex = __instance.currentBehaviourStateIndex,
previousState = __instance.previousState, 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))] [HarmonyPatch(nameof(JesterAI.Update))]
[HarmonyPostfix] [HarmonyPostfix]
static void JesterUpdatePostfix(JesterAI __instance, State __state) 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 #if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} CurrentTrack is not set!"); Debug.LogError($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
#endif #endif
return; return;
} }
if (__instance.previousState == 1 && __state.previousState != 1) // This switch statement resembles the one from JesterAI.Update
switch (__state.currentBehaviourStateIndex)
{ {
// if just started winding up case 1:
// then stop the default music... if (__state.previousState != 1)
__instance.farAudio.Stop(); {
__instance.creatureVoice.Stop(); // if just started winding up
// 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 // Set up custom popup timer, which is shorter than Intro audio
Plugin.BeatTimeState = new BeatTimeState(Plugin.CurrentTrack); __instance.popUpTimer = behaviour.CurrentTrack.WindUpTimer;
// Set up custom popup timer, which is shorter than Start audio
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
// Override popGoesTheWeaselTheme with Start audio if (Config.ShouldSkipWindingPhase)
__instance.farAudio.maxDistance = Plugin.AudioMaxDistance; {
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedIntro; var rewind = 5f;
__instance.farAudio.loop = false; __instance.popUpTimer = rewind;
if (Config.ShouldSkipWindingPhase) introAudioSource.time = behaviour.CurrentTrack.WindUpTimer - rewind;
{ }
var rewind = 5f; else
__instance.popUpTimer = rewind; {
__instance.farAudio.time = Plugin.CurrentTrack.WindUpTimer - rewind; // reset if previously skipped winding by assigning different starting time.
} introAudioSource.time = 0f;
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}"); __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;
} }
if (__instance.previousState != 2 && __state.previousState == 2) // transition away from state 2 ("poppedOut"), normally to state 0
if (__state.previousState == 2 && __instance.previousState != 2)
{ {
Plugin.ResetLightColor(); Plugin.ResetLightColor();
DiscoBallManager.Disable(); DiscoBallManager.Disable();
// Rotate track groups // Rotate track groups
__instance.GetComponent<MuzikaGromcheJesterNetworkBehaviour>()?.ChooseTrackServerRpc(); behaviour.ChooseTrackServerRpc();
} behaviour.BeatTimeState = null;
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}");
} }
// Manage the timeline: switch color of the lights according to the current playback/beat position. // 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) foreach (var ev in events)
{ {
switch (ev) switch (ev)
@ -2465,8 +2491,8 @@ namespace MuzikaGromche
Plugin.ResetLightColor(); Plugin.ResetLightColor();
DiscoBallManager.Disable(); DiscoBallManager.Disable();
// Just in case if players have spawned multiple Jesters, // Just in case if players have spawned multiple Jesters,
// Don't reset Plugin.CurrentTrack and Plugin.BeatTimeState to null, // Don't reset Plugin.CurrentTrack to null,
// so that the code wouldn't crash without extra null checks. // so that the latest chosen track remains set.
} }
} }
} }

View File

@ -252,6 +252,7 @@ namespace MuzikaGromche
static bool OnRefreshLightsList(RoundManager __instance) static bool OnRefreshLightsList(RoundManager __instance)
{ {
RefreshLightsListPatched(__instance); RefreshLightsListPatched(__instance);
LoadInitialLightsColors(__instance);
// Skip the original method // Skip the original method
return false; return false;
} }
@ -286,5 +287,15 @@ namespace MuzikaGromche
animator.SetFloat("flickerSpeed", UnityEngine.Random.Range(0.6f, 1.4f)); 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 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. 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. 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/ [`LethalConfig`]: https://thunderstore.io/c/lethal-company/p/AinaVT/LethalConfig/
[`LobbyCompatibility`]: https://thunderstore.io/c/lethal-company/p/BMX/LobbyCompatibility/ [`LobbyCompatibility`]: https://thunderstore.io/c/lethal-company/p/BMX/LobbyCompatibility/
[`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/ [`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/

View File

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