forked from nikita/muzika-gromche
Compare commits
17 Commits
6a9ea8d4af
...
63de62111f
Author | SHA1 | Date |
---|---|---|
|
63de62111f | |
|
4cc9713fa7 | |
|
8710df7525 | |
|
9d23fd5b95 | |
|
4516b853cd | |
|
b3767cbbf0 | |
|
327e606deb | |
|
70e45d5ba2 | |
|
d4d3e15de3 | |
|
525c0e108f | |
|
73ad702684 | |
|
e67de4556c | |
|
0b0383003f | |
|
9ed98197f8 | |
|
fe5752cbff | |
|
c6b128270f | |
|
852d866073 |
BIN
Assets/BeefLiverLoop.ogg (Stored with Git LFS)
BIN
Assets/BeefLiverLoop.ogg (Stored with Git LFS)
Binary file not shown.
BIN
Assets/BehaLoop.ogg (Stored with Git LFS)
BIN
Assets/BehaLoop.ogg (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue