1
0
Fork 0

Compare commits

...

24 Commits

Author SHA1 Message Date
ivan tkachenko d59c5a20c1 Add Thunderstore config for automated uploading 2026-01-12 04:00:33 +02:00
ivan tkachenko b1d449cf02 Release v1337.9001.4 2026-01-12 03:22:14 +02:00
ivan tkachenko 3f06cc9aa6 Add new track PickUpSticks 2026-01-12 03:20:03 +02:00
ivan tkachenko a5659fcb09 README: Include a link to an upcoming HookahPlace mod 2026-01-11 16:15:18 +02:00
ivan tkachenko 6271a377bd README: Describe recently added tracks 2026-01-11 16:15:18 +02:00
ivan tkachenko a4cee92d00 Load audio clips on demand, implement cache
Reduces cold-boot memory usage by 400 MB for the current playlist of
58 audio files (27.8 MB).
2026-01-11 16:06:45 +02:00
ivan tkachenko f83f2a72ba Mark AudioClip as nullable 2026-01-11 03:17:49 +02:00
ivan tkachenko afb3e34e71 Implement seasonal content framework
to ensure that New Year's songs won't play in summer.
2026-01-11 02:53:53 +02:00
ivan tkachenko ebd7811b12 Avoid null dereference while reading seed in orbit 2026-01-11 02:13:19 +02:00
ivan tkachenko a64d671527 Add Config.ReduceVFXIntensity option 2026-01-11 00:12:21 +02:00
ivan tkachenko 7eaa5fce75 Add new track DiscoKapot 2026-01-10 23:47:39 +02:00
ivan tkachenko da86ca6a2d Add new track Paarden 2026-01-10 22:51:56 +02:00
ivan tkachenko c4c1919df6 Adjust lyrics for PWNED 2026-01-10 21:10:08 +02:00
ivan tkachenko 869d982b1e Remaster recently added track IkWilJe, rework visual effects 2026-01-10 21:07:15 +02:00
ivan tkachenko 10839ba22c fixup CHANGELOG 2026-01-10 19:45:28 +02:00
ivan tkachenko 398de3dc04 Bump version 2026-01-10 19:41:07 +02:00
ivan tkachenko 4f432968ef Release v1337.9001.3 2025-12-30 23:40:33 +02:00
ivan tkachenko 56cea50a65 add new track IkWilJe 2025-12-30 23:39:01 +02:00
ivan tkachenko 0d416c6f5a Release v1337.9001.2 2025-12-30 22:51:39 +02:00
ivan tkachenko c1d91839e4 add new track HighLow 2025-12-30 22:25:50 +02:00
ivan tkachenko 76189c6ad2 Update BepInEx.PluginInfoProps to version 2.x
2.x implements better namespacing.
2025-12-30 22:25:49 +02:00
ivan tkachenko b6f576d50d Include debug symbols, but strip sensitive source paths 2025-12-20 20:35:15 +02:00
ivan tkachenko a4ca1c86ec Save Harmony own instance in private static
That's how other mods do it. Might be useful to reload patches.
2025-12-19 23:40:46 +02:00
ivan tkachenko 38c9472cb1 Port logging to BepInEx ManualLogSource
- pros: free namespace by default
- cons: Debug level has to be enabled manually in BepInEx.cfg,
  specifically in the section named [Logging.Console]
2025-12-19 23:39:28 +02:00
25 changed files with 722 additions and 127 deletions

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -1,7 +1,23 @@
# Changelog
## MuzikaGromche 1337.9001.2
## MuzikaGromche 1337.9001.4 - v73 Chinese New Year Edition
- Remastered recently added track IkWilJe using a higher quality source audio and better fitting visual effects.
- Adjusted lyrics for PWNED (can't believe it missed an obvious joke).
- Added a new track Paarden.
- Added a new track DiscoKapot.
- Added an accessibility option to reduce the intensity of overly distracting visual effects.
- Seasonal content like New Year's songs (IkWilJe, Paarden, DiscoKapot) will only be available for selection during their respective seasons.
- Reduced memory usage by almost 400 MB, thanks to loading audio clips on demand (not preloading all tracks at launch).
- Added a new track PickUpSticks.
## MuzikaGromche 1337.9001.3 - v73 Happy New Year Edition
- Added a new track IkWilJe.
## MuzikaGromche 1337.9001.2 - v73 Rushed Edition
- Added a new track HighLow.
## MuzikaGromche 1337.9001.1 - v73 Music louder Edition

View File

@ -0,0 +1,170 @@
using HarmonyLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Networking;
namespace MuzikaGromche;
internal static class AudioClipsCacheManager
{
// Cache of file names to loaded AudioClips.
// Cache is cleared at the end of each round.
static readonly Dictionary<string, AudioClip> Cache = [];
// In-flight requests
static readonly Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> Requests = [];
// Not just isDone status, but also whether all requests have been processed.
public static bool AllDone => Requests.Count == 0;
public static void LoadAudioTrack(IAudioTrack track)
{
GlobalBehaviour.Instance.StartCoroutine(LoadAudioTrackCoroutine(track));
}
static IEnumerator LoadAudioTrackCoroutine(IAudioTrack track)
{
List<UnityWebRequest> requests = [];
requests.Capacity = 2;
LoadAudioClip(requests, track.AudioType, track.FileNameIntro, clip => track.LoadedIntro = clip);
LoadAudioClip(requests, track.AudioType, track.FileNameLoop, clip => track.LoadedLoop = clip);
yield return new WaitUntil(() => requests.All(request => request.isDone));
if (requests.All(request => request.result == UnityWebRequest.Result.Success))
{
foreach (var request in requests)
{
foreach (var (fileName, (Request, Setters)) in Requests)
{
if (request == Request)
{
Plugin.Log.LogDebug($"Audio clip loaded successfully: {fileName}");
var clip = DownloadHandlerAudioClip.GetContent(request);
Cache[fileName] = clip;
foreach (var setter in Setters)
{
setter(clip);
}
}
}
}
}
else
{
var failed = Requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl());
Plugin.Log.LogError("Could not load audio file " + string.Join(", ", failed));
}
// cleanup
foreach (var request in requests)
{
// collect matching keys first to avoid mutating Requests while iterating it
var fileNames = Requests
.Where(kv => kv.Value.Request == request)
.Select(kv => kv.Key)
.ToArray();
foreach (var fileName in fileNames)
{
if (Requests.TryGetValue(fileName, out var tuple) && tuple.Request != null)
{
tuple.Request.Dispose();
}
Requests.Remove(fileName);
}
}
}
static readonly string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
static void LoadAudioClip(List<UnityWebRequest> requests, AudioType audioType, string fileName, Action<AudioClip> setter)
{
if (Cache.TryGetValue(fileName, out var cachedClip))
{
Plugin.Log.LogDebug($"Found cached audio clip: {fileName}");
setter(cachedClip);
}
else if (Requests.TryGetValue(fileName, out var tuple))
{
Plugin.Log.LogDebug($"Found existing in-flight request for audio clip: {fileName}");
tuple.Setters.Add(setter);
}
else
{
Plugin.Log.LogDebug($"Sending request to load audio clip: {fileName}");
var request = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{fileName}", audioType);
request.SendWebRequest();
Requests[fileName] = (request, [setter]);
requests.Add(request);
}
}
public static void Clear()
{
// Iterate over LoadedClipsCache keys and values, cross join with Plugin.Tracks list,
// find AudioTracks that reference the key (file name), and null their corresponding loaded tracks;
// then destroy tracks and clear the cache.
Plugin.Log.LogDebug($"Clearing {Cache.Count} cached audio clips and {Requests.Count} pending requests");
if (Cache.Count > 0)
{
var allTracks = Plugin.Tracks.SelectMany(t => t.GetTracks()).ToArray();
foreach (var (fileName, clip) in Cache)
{
foreach (var track in allTracks)
{
// Null out any references to this clip on matching file names.
if (track.FileNameIntro == fileName)
{
track.LoadedIntro = null;
}
if (track.FileNameLoop == fileName)
{
track.LoadedLoop = null;
}
}
if (clip != null)
{
UnityEngine.Object.Destroy(clip);
}
}
Cache.Clear();
}
foreach (var (fileName, (Request, Setters)) in Requests)
{
if (Request != null)
{
Request.Abort();
Request.Dispose();
}
}
Requests.Clear();
}
}
[HarmonyPatch(typeof(RoundManager))]
static class ClearAudioClipCachePatch
{
[HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
[HarmonyPrefix]
static void OnDestroy(RoundManager __instance)
{
var _ = __instance;
AudioClipsCacheManager.Clear();
}
}

View File

@ -82,7 +82,7 @@ namespace MuzikaGromche
CachedDiscoBalls.Add(discoBall);
discoBall.SetActive(false);
Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Patched tile '{tile.gameObject.name}'");
Plugin.Log.LogDebug($"{nameof(DiscoBallManager)} Patched tile '{tile.gameObject.name}'");
}
static IEnumerable<Animator> FindDiscoBallAnimators(GameObject discoBall)
@ -109,7 +109,7 @@ namespace MuzikaGromche
public static void Toggle(bool on)
{
Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Toggle {(on ? "ON" : "OFF")} {CachedDiscoBallAnimators.Count} animators");
Plugin.Log.LogDebug($"{nameof(DiscoBallManager)} Toggle {(on ? "ON" : "OFF")} {CachedDiscoBallAnimators.Count} animators");
foreach (var discoBall in CachedDiscoBalls)
{
@ -133,7 +133,7 @@ namespace MuzikaGromche
internal static void Clear()
{
Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Clearing {CachedDiscoBalls.Count} disco balls & {CachedDiscoBallAnimators.Count} animators");
Plugin.Log.LogDebug($"{nameof(DiscoBallManager)} Clearing {CachedDiscoBalls.Count} disco balls & {CachedDiscoBallAnimators.Count} animators");
CachedDiscoBallAnimators.Clear();
CachedDiscoBalls.Clear();
}

View File

@ -20,7 +20,7 @@ namespace MuzikaGromche
var jsonObject = new Dictionary<string, object>();
var tracksList = new List<object>();
jsonObject["version"] = PluginInfo.PLUGIN_VERSION;
jsonObject["version"] = MyPluginInfo.PLUGIN_VERSION;
jsonObject["tracks"] = tracksList;
foreach (var (selectableTrack, audioTrack) in SelectTracks(tracks))
{
@ -56,14 +56,15 @@ namespace MuzikaGromche
{
["Name"] = audioTrack.Name, // may be different from selectableTrack.Name, if selectable track is a group
["IsExplicit"] = selectableTrack.IsExplicit,
["Season"] = selectableTrack.Season?.Name,
["Language"] = selectableTrack.Language.Full,
["WindUpTimer"] = audioTrack.WindUpTimer,
["Bpm"] = audioTrack.Bpm,
["Beats"] = audioTrack.Beats,
["LoopOffset"] = audioTrack.LoopOffset,
["Ext"] = audioTrack.Ext,
["FileDurationIntro"] = audioTrack.LoadedIntro.length,
["FileDurationLoop"] = audioTrack.LoadedLoop.length,
["FileDurationIntro"] = audioTrack.LoadedIntro?.length ?? 0f,
["FileDurationLoop"] = audioTrack.LoadedLoop?.length ?? 0f,
["FileNameIntro"] = audioTrack.FileNameIntro,
["FileNameLoop"] = audioTrack.FileNameLoop,
["BeatsOffset"] = audioTrack.BeatsOffset,

View File

@ -0,0 +1,30 @@
using UnityEngine;
namespace MuzikaGromche;
// A global MonoBehaviour instance to run coroutines from non-MonoBehaviour or static context.
internal static class GlobalBehaviour
{
sealed class AdhocBehaviour : MonoBehaviour;
static AdhocBehaviour? instance = null;
public static MonoBehaviour Instance
{
get
{
if (instance == null)
{
var go = new GameObject("MuzikaGromche_GlobalBehaviour", [
typeof(AdhocBehaviour),
])
{
hideFlags = HideFlags.HideAndDontSave
};
Object.DontDestroyOnLoad(go);
instance = go.GetComponent<AdhocBehaviour>();
}
return instance;
}
}
}

View File

@ -8,14 +8,11 @@
<AssemblyName>Ratijas.MuzikaGromche</AssemblyName>
<Product>Muzika Gromche</Product>
<Description>Add some content to your inverse teleporter experience on Titan!</Description>
<Version>1337.9001.2</Version>
<Version>1337.9001.4</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<!-- NetcodePatch requires anything but 'full' -->
<DebugType>portable</DebugType>
<PackageReadmeFile>../README.md</PackageReadmeFile>
<PackageProjectUrl>https://git.vilunov.me/ratijas/muzika-gromche</PackageProjectUrl>
<RepositoryUrl>https://git.vilunov.me/ratijas/muzika-gromche</RepositoryUrl>
@ -32,10 +29,20 @@
<NoWarn>$(NoWarn);CS0436</NoWarn>
</PropertyGroup>
<!-- Embedded debug -->
<PropertyGroup>
<DebugSymbols>true</DebugSymbols>
<!-- NetcodePatch requires anything but 'full' -->
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="$(Configuration) == 'Release'">
<PathMap>$(UserProfile)=~,$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=$(PackageId)/</PathMap>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.Core" Version="5.*" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" PrivateAssets="all"/>
<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="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />

View File

@ -1,5 +1,6 @@
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using LethalConfig;
using LethalConfig.ConfigItems;
@ -7,7 +8,6 @@ using LethalConfig.ConfigItems.Options;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Net.Sockets;
@ -16,16 +16,17 @@ using System.Security.Cryptography;
using System.Text;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Networking;
namespace MuzikaGromche
{
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
[BepInDependency("ainavt.lc.lethalconfig", "1.4.6")]
[BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)]
[BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.SoftDependency)]
public class Plugin : BaseUnityPlugin
{
private static Harmony Harmony = null!;
internal static ManualLogSource Log = 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.
@ -46,6 +47,15 @@ namespace MuzikaGromche
[-0.5f, 0.5f, 8f, 15f, 16f, 24f, 29f, 30f, 36f, 37f, 38f, 44f, 47.5f],
[ 0f, 0.6f, 0f, 0f, 0.4f, 0f, 0f, 0.3f, 0f, 0f, 0.3f, 0f, 0f]);
private static readonly Palette PalettePickUpSticks = Palette
.Parse(["#FC933C", "#FC3C9D", "#EEA0A5", "#CA71FC", "#d01760"])
.Use(p =>
{
var energetic = p * 6 + new Palette(p.Colors[0..2]); // 32 colors
var slower = (p * 3 + new Palette([p.Colors[2]])).Stretch(2); // 16*2 colors
return energetic + slower;
});
public static readonly ISelectableTrack[] Tracks = [
new SelectableAudioTrack
{
@ -458,8 +468,8 @@ namespace MuzikaGromche
(56, "Counting crypto to\nembarrass Wall Street"),
(80, $"Instling min3r.exe\t\t\tresolving ur private IP\n/"),
(82, $"Instling min3r.exe\n00% [8=D ]\tHenllo ${{username = \"{Environment.UserName}\"}}\t\tresolving ur private IP\n-{PwnLyricsVariants[^3]}"),
(84, $"Instling min3r.exe\n33% [8====D ]\t\t\tresolving ur private IP\n\\{PwnLyricsVariants[^3]}"),
(86, $"Instling min3r.exe\n66% [8=========D ]\t\t\tresolving ur private IP\n|{PwnLyricsVariants[^2]}"),
(84, $"Instling min3r.exe\n34% [8====D ]\t\t\tresolving ur private IP\n\\{PwnLyricsVariants[^3]}"),
(86, $"Instling min3r.exe\n69% [8=========D ]\t\t\tresolving ur private IP\n|{PwnLyricsVariants[^2]}"),
(88, $"Instling min3r.exe\n95% [8============D ]\t\tWhere did you download\nthis < mod / dll > from?\tresolving ur private IP\n{PwnLyricsVariants[^2]}/"),
(90, $"Instling min3r.exe\n99% [8=============D]\t\t\tresolving ur private IP\n-{PwnLyricsVariants[^2]}"),
(92, $"Encrpt1ng f!les.. \n99% [8=============D]\t\t\tresolving ur private IP\n\\{PwnLyricsVariants[^1]}"),
@ -847,17 +857,192 @@ namespace MuzikaGromche
[0f, 0.5f, 0f, 0f, 0.5f]),
GameOverText = "[LIFE SUPPORT: REAL GONE]",
},
new SelectableAudioTrack
{
Name = "HighLow",
AudioType = AudioType.OGGVORBIS,
Language = Language.ENGLISH,
WindUpTimer = 37.12f,
Bars = 12,
BeatsOffset = 0f,
ColorTransitionIn = 0.75f,
ColorTransitionOut = 0.25f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse([
"#2e2e28", "#dfa24d", "#2e2e28", "#dfa24d",
"#2e2e28", "#dfa24d", "#2e2e28", "#dfa24d",
]),
LoopOffset = 0,
FadeOutBeat = -1.5f,
FadeOutDuration = 1.5f,
FlickerLightsTimeSeries = [-33, 39],
Lyrics = [
],
DrunknessLoopOffsetTimeSeries = new(
[-2f, -1f, 6f],
[ 0f, 0.5f, 0f]),
CondensationLoopOffsetTimeSeries = new(
[-2f, -1f, 6f],
[ 0f, 0.5f, 0f]),
GameOverText = "[LIFE SUPORT: NIRVANA]",
},
new SelectableAudioTrack
{
Name = "IkWilJe",
AudioType = AudioType.OGGVORBIS,
Language = Language.ENGLISH,
Season = Season.NewYear,
WindUpTimer = 43.03f,
Beats = 13 * 4 + 2, // = 54
BeatsOffset = 0f,
ColorTransitionIn = 0.01f,
ColorTransitionOut = 0.99f,
ColorTransitionEasing = Easing.OutExpo,
Palette = Palette.Parse([
"#0B6623", "#FF2D2D", "#FFD700",
"#00BFFF", "#9400D3", "#00FF7F",
]),
LoopOffset = 0,
FadeOutBeat = -14f,
FadeOutDuration = 12f,
FlickerLightsTimeSeries = [31.45f],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new(
[0f, 0.25f, 6f],
[0f, 0.5f, 0f]),
GameOverText = "[NEXT YEAR -- DEFINITELY]",
},
new SelectableAudioTrack
{
Name = "Paarden",
AudioType = AudioType.OGGVORBIS,
Language = Language.RUSSIAN,
Season = Season.NewYear,
WindUpTimer = 36.12f,
Bars = 8,
BeatsOffset = 0f,
ColorTransitionIn = 0.25f,
ColorTransitionOut = 0.4f,
ColorTransitionEasing = Easing.OutCubic,
Palette = Palette.Parse([
"#F0FBFF", "#9ED9FF", "#0B95FF",
"#66C7FF", "#CAE8FF", "#3BB6FF",
]),
LoopOffset = 0,
FadeOutBeat = -4f,
FadeOutDuration = 4f,
FlickerLightsTimeSeries = [31.5f],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new(
[0f, 0.25f, 6f],
[0f, 0.5f, 0f]),
GameOverText = "[NEXT YEAR -- DEFINITELY]",
},
new SelectableAudioTrack
{
Name = "DiscoKapot",
AudioType = AudioType.OGGVORBIS,
Language = Language.RUSSIAN,
Season = Season.NewYear,
WindUpTimer = 30.3f,
Bars = 8,
BeatsOffset = 0f,
ColorTransitionIn = 0.25f,
ColorTransitionOut = 0.6f,
ColorTransitionEasing = Easing.InOutExpo,
Palette = Palette.Parse([
"#0B6623", "#FF2D2D", "#FFD700",
"#00BFFF", "#9400D3", "#00FF7F",
]),
LoopOffset = 0,
FadeOutBeat = -4f,
FadeOutDuration = 4f,
FlickerLightsTimeSeries = [-32, -24, -16, 16, 32],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new(
[0f, 0.25f, 6f],
[0f, 0.5f, 0f]),
GameOverText = "[NEXT YEAR -- DEFINITELY]",
},
new SelectableTracksGroup
{
Name = "PickUpSticks",
Language = Language.ENGLISH,
Tracks =
[
new CoreAudioTrack
{
Name = "PickUpSticks1",
FileNameLoop = "PickUpSticksLoop.ogg",
AudioType = AudioType.OGGVORBIS,
WindUpTimer = 38.5f,
Bars = 16,
BeatsOffset = 0.2f,
ColorTransitionIn = 0.6f,
ColorTransitionOut = 0.3f,
ColorTransitionEasing = Easing.InOutCubic,
Palette = PalettePickUpSticks,
LoopOffset = 0,
FadeOutBeat = -2,
FadeOutDuration = 2,
FlickerLightsTimeSeries = [-36, -4, 32],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new([0f, 0.5f, 3f, 32f, 34f, 40f], [0f, 0.5f, 0f, 0f, 0.3f, 0f]),
CondensationLoopOffsetTimeSeries = new([23f, 28f, 31f, 34f, 38f, 52f], [0f, 0.6f, 0f, 0f, 0.7f, 0f]),
GameOverText = "[LOVE SUPPORT: OFFLINE]",
},
new CoreAudioTrack
{
Name = "PickUpSticks2",
FileNameLoop = "PickUpSticksLoop.ogg",
AudioType = AudioType.OGGVORBIS,
WindUpTimer = 38.47f,
Bars = 16,
BeatsOffset = 0.2f,
ColorTransitionIn = 0.6f,
ColorTransitionOut = 0.3f,
ColorTransitionEasing = Easing.InOutCubic,
Palette = PalettePickUpSticks,
LoopOffset = 0,
FadeOutBeat = -2,
FadeOutDuration = 2,
FlickerLightsTimeSeries = [-36, -4, 32],
Lyrics = [],
DrunknessLoopOffsetTimeSeries = new([0f, 0.5f, 3f, 32f, 34f, 40f], [0f, 0.5f, 0f, 0f, 0.3f, 0f]),
CondensationLoopOffsetTimeSeries = new([23f, 28f, 31f, 34f, 38f, 52f], [0f, 0.5f, 0f, 0f, 0.5f, 0f]),
GameOverText = "[LOVE SUPPORT: OFFLINE]",
},
],
},
];
private static int GetCurrentSeed()
{
var seed = 0;
var roundManager = RoundManager.Instance;
if (roundManager != null && roundManager.dungeonGenerator != null)
{
seed = roundManager.dungeonGenerator.Generator.ChosenSeed;
}
return seed;
}
public static ISelectableTrack ChooseTrack()
{
var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed;
var tracks = Config.SkipExplicitTracks.Value ? [.. Tracks.Where(track => !track.IsExplicit)] : Tracks;
int[] weights = [.. tracks.Select(track => track.Weight.Value)];
var seed = GetCurrentSeed();
var today = DateTime.Today;
var season = SeasonalContentManager.CurrentSeason(today);
var tracksEnumerable = SeasonalContentManager.Filter(Tracks, season);
if (Config.SkipExplicitTracks.Value)
{
tracksEnumerable = tracksEnumerable.Where(track => !track.IsExplicit);
}
var tracks = tracksEnumerable.ToArray();
int[] weights = tracks.Select(track => track.Weight.Value).ToArray();
var rwi = new RandomWeightedIndex(weights);
var trackId = rwi.GetRandomWeightedIndex(seed);
var track = tracks[trackId];
Debug.Log($"{nameof(MuzikaGromche)} Seed is {seed}, chosen track is \"{track.Name}\", #{trackId} of {rwi}");
Log.LogInfo($"Seed is {seed}, season is {season?.Name ?? "<none>"}, chosen track is \"{track.Name}\", #{trackId} of {rwi}");
return tracks[trackId];
}
@ -890,78 +1075,52 @@ namespace MuzikaGromche
void Awake()
{
Log = Logger;
// Sort in place by name
Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks);
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> requests = [];
requests.EnsureCapacity(Tracks.Length * 2);
foreach (var track in Tracks.SelectMany(track => track.GetTracks()))
{
foreach (var (fileName, setter) in new (string, Action<AudioClip>)[]
{
(track.FileNameIntro, clip => track.LoadedIntro = clip),
(track.FileNameLoop, clip => track.LoadedLoop = clip),
})
{
if (requests.TryGetValue(fileName, out var tuple))
{
tuple.Setters.Add(setter);
}
else
{
var request = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{fileName}", track.AudioType);
request.SendWebRequest();
requests[fileName] = (request, [setter]);
}
}
}
while (!requests.Values.All(tuple => tuple.Request.isDone)) { }
if (requests.Values.All(tuple => tuple.Request.result == UnityWebRequest.Result.Success))
{
foreach (var (fileName, tuple) in requests)
{
var clip = DownloadHandlerAudioClip.GetContent(tuple.Request);
foreach (var setter in tuple.Setters)
{
setter(clip);
}
}
#if DEBUG
foreach (var track in Tracks)
{
track.Debug();
}
Exporter.ExportTracksJSON(Tracks);
GlobalBehaviour.Instance.StartCoroutine(PreloadDebugAndExport(Tracks));
#endif
Config = new Config(base.Config);
DiscoBallManager.Load();
PoweredLightsAnimators.Load();
var harmony = new Harmony(PluginInfo.PLUGIN_NAME);
harmony.PatchAll(typeof(GameNetworkManagerPatch));
harmony.PatchAll(typeof(JesterPatch));
harmony.PatchAll(typeof(EnemyAIPatch));
harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch));
harmony.PatchAll(typeof(AllPoweredLightsPatch));
harmony.PatchAll(typeof(DiscoBallTilePatch));
harmony.PatchAll(typeof(DiscoBallDespawnPatch));
harmony.PatchAll(typeof(SpawnRatePatch));
harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
NetcodePatcher();
Compatibility.Register(this);
}
else
{
var failed = requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl());
Logger.LogError("Could not load audio file " + string.Join(", ", failed));
}
Config = new Config(base.Config);
DiscoBallManager.Load();
PoweredLightsAnimators.Load();
Harmony = new Harmony(MyPluginInfo.PLUGIN_NAME);
Harmony.PatchAll(typeof(GameNetworkManagerPatch));
Harmony.PatchAll(typeof(JesterPatch));
Harmony.PatchAll(typeof(EnemyAIPatch));
Harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch));
Harmony.PatchAll(typeof(AllPoweredLightsPatch));
Harmony.PatchAll(typeof(DiscoBallTilePatch));
Harmony.PatchAll(typeof(DiscoBallDespawnPatch));
Harmony.PatchAll(typeof(SpawnRatePatch));
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
NetcodePatcher();
Compatibility.Register(this);
}
#if DEBUG
static IEnumerator PreloadDebugAndExport(ISelectableTrack[] tracks)
{
foreach (var track in tracks.SelectMany(track => track.GetTracks()))
{
AudioClipsCacheManager.LoadAudioTrack(track);
}
yield return new WaitUntil(() => AudioClipsCacheManager.AllDone);
Log.LogDebug("All tracks preloaded, exporting to JSON");
foreach (var track in tracks)
{
track.Debug();
}
Exporter.ExportTracksJSON(tracks);
AudioClipsCacheManager.Clear();
}
#endif
private static void NetcodePatcher()
{
var types = Assembly.GetExecutingAssembly().GetTypes();
@ -1092,7 +1251,7 @@ namespace MuzikaGromche
// 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
public interface ISelectableTrack : ISeasonalContent
{
// Name of the track, as shown in config entry UI; also used for default file names.
public string Name { get; init; }
@ -1129,21 +1288,47 @@ namespace MuzikaGromche
public float WindUpTimer { get; }
// Estimated number of beats per minute. Not used for light show, but might come in handy.
public float Bpm => 60f / (LoadedLoop.length / Beats);
public float Bpm
{
get
{
if (LoadedLoop == null || LoadedLoop.length <= 0f)
{
return 0f;
}
else
{
return 60f / (LoadedLoop.length / Beats);
}
}
}
// How many beats the loop segment has. The default strategy is to switch color of lights on each beat.
public int Beats { get; }
// Number of beats between WindUpTimer and where looped segment starts (not the loop audio).
public int LoopOffset { get; }
public float LoopOffsetInSeconds => (float)LoopOffset / (float)Beats * LoadedLoop.length;
public float LoopOffsetInSeconds
{
get
{
if (LoadedLoop == null || LoadedLoop.length <= 0f)
{
return 0f;
}
else
{
return (float)LoopOffset / (float)Beats * LoadedLoop.length;
}
}
}
// MPEG is basically mp3, and it can produce gaps at the start.
// WAV is OK, but takes a lot of space. Try OGGVORBIS instead.
public AudioType AudioType { get; }
public AudioClip LoadedIntro { get; internal set; }
public AudioClip LoadedLoop { get; internal set; }
public AudioClip? LoadedIntro { get; internal set; }
public AudioClip? LoadedLoop { get; internal set; }
public string FileNameIntro { get; }
public string FileNameLoop { get; }
@ -1160,7 +1345,20 @@ namespace MuzikaGromche
public float BeatsOffset { get; }
// Offset of beats, in seconds. Bigger offset => colors will change later.
public float BeatsOffsetInSeconds => BeatsOffset / Beats * LoadedLoop.length;
public float BeatsOffsetInSeconds
{
get
{
if (LoadedLoop == null || LoadedLoop.length <= 0f)
{
return 0f;
}
else
{
return BeatsOffset / (float)Beats * LoadedLoop.length;
}
}
}
public float FadeOutBeat { get; }
public float FadeOutDuration { get; }
@ -1199,8 +1397,8 @@ namespace MuzikaGromche
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; }
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;
@ -1234,8 +1432,8 @@ namespace MuzikaGromche
public int LoopOffset { get; init; } = 0;
public AudioType AudioType { get; init; } = AudioType.MPEG;
public AudioClip LoadedIntro { get; set; } = null!;
public AudioClip LoadedLoop { get; set; } = null!;
public AudioClip? LoadedIntro { get; set; } = null;
public AudioClip? LoadedLoop { get; set; } = null;
private string? FileNameIntroOverride = null;
public string FileNameIntro
@ -1303,6 +1501,7 @@ namespace MuzikaGromche
{
public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
public Season? Season { get; init; } = null;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
@ -1311,7 +1510,7 @@ namespace MuzikaGromche
void ISelectableTrack.Debug()
{
Debug.Log($"{nameof(MuzikaGromche)} Track \"{Name}\", Intro={LoadedIntro.length:N4}, Loop={LoadedLoop.length:N4}");
Plugin.Log.LogDebug($"Track \"{Name}\", Intro={LoadedIntro?.length:N4}, Loop={LoadedLoop?.length:N4}");
}
}
@ -1320,6 +1519,7 @@ namespace MuzikaGromche
public /* required */ string Name { get; init; } = "";
public /* required */ Language Language { get; init; }
public bool IsExplicit { get; init; } = false;
public Season? Season { get; init; } = null;
ConfigEntry<int> ISelectableTrack.Weight { get; set; } = null!;
public /* required */ IAudioTrack[] Tracks = [];
@ -1337,10 +1537,10 @@ namespace MuzikaGromche
void ISelectableTrack.Debug()
{
Debug.Log($"{nameof(MuzikaGromche)} Track Group \"{Name}\", Count={Tracks.Length}");
Plugin.Log.LogDebug($"Track Group \"{Name}\", Count={Tracks.Length}");
foreach (var (track, index) in Tracks.Select((x, i) => (x, i)))
{
Debug.Log($"{nameof(MuzikaGromche)} Track {index} \"{track.Name}\", Intro={track.LoadedIntro.length:N4}, Loop={track.LoadedLoop.length:N4}");
Plugin.Log.LogDebug($" Track {index} \"{track.Name}\", Intro={track.LoadedIntro?.length:N4}, Loop={track.LoadedLoop?.length:N4}");
}
}
}
@ -1707,7 +1907,7 @@ namespace MuzikaGromche
float timeSinceStartOfLoop = time - offset;
var adjustedTimeNormalized = timeSinceStartOfLoop / LoopLength;
var adjustedTimeNormalized = (LoopLength <= 0f) ? 0f : timeSinceStartOfLoop / LoopLength;
var beat = adjustedTimeNormalized * Beats;
@ -1717,11 +1917,10 @@ namespace MuzikaGromche
IsLooping |= timestamp.IsLooping;
#if DEBUG && false
Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} {3} Time={4:N4} norm={5,6:N4} beat={6,7:N4}",
nameof(MuzikaGromche),
Plugin.Log.LogDebug(string.Format("t={0,10:N4} d={1,7:N4} {2} Time={3:N4} norm={4,6:N4} beat={5,7:N4}",
Time.realtimeSinceStartup, Time.deltaTime,
isExtrapolated ? 'E' : '_', time,
adjustedTimeNormalized, beat);
adjustedTimeNormalized, beat));
#endif
return timestamp;
@ -1756,9 +1955,10 @@ namespace MuzikaGromche
LyricsRandomPerLoop = LyricsRandom.Next();
}
this.track = track;
AudioState = new(track.LoadedIntro.length);
WindUpLoopingState = new(track.WindUpTimer, track.LoadedLoop.length, track.Beats);
LoopLoopingState = new(track.WindUpTimer + track.LoopOffsetInSeconds, track.LoadedLoop.length, track.Beats);
AudioState = new(track.LoadedIntro?.length ?? 0f);
var loadedLoopLength = track.LoadedLoop?.length ?? 0f;
WindUpLoopingState = new(track.WindUpTimer, loadedLoopLength, track.Beats);
LoopLoopingState = new(track.WindUpTimer + track.LoopOffsetInSeconds, loadedLoopLength, track.Beats);
}
public List<BaseEvent> Update(AudioSource intro, AudioSource loop)
@ -1783,7 +1983,7 @@ namespace MuzikaGromche
LastKnownLoopOffsetBeat = loopOffsetTimestamp.Beat;
var events = GetEvents(loopOffsetTimestamp, loopOffsetSpan, windUpOffsetTimestamp);
#if DEBUG
Debug.Log($"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}");
Plugin.Log.LogDebug($"looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(",", events)}");
#endif
return events;
}
@ -1842,7 +2042,8 @@ namespace MuzikaGromche
if (GetInterpolation(loopOffsetTimestamp, track.DrunknessLoopOffsetTimeSeries, Easing.Linear) is { } drunkness)
{
events.Add(new DrunkEvent(drunkness));
var value = Config.ReduceVFXIntensity.Value ? drunkness * 0.3f : drunkness;
events.Add(new DrunkEvent(value));
}
if (GetInterpolation(loopOffsetTimestamp, track.CondensationLoopOffsetTimeSeries, Easing.Linear) is { } condensation)
@ -2233,6 +2434,8 @@ namespace MuzikaGromche
{
public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!;
public static ConfigEntry<bool> ReduceVFXIntensity { get; private set; } = null!;
public static ConfigEntry<float> AudioOffset { get; private set; } = null!;
public static ConfigEntry<bool> SkipExplicitTracks { get; private set; } = null!;
@ -2299,6 +2502,10 @@ namespace MuzikaGromche
new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(DisplayLyrics, requiresRestart: false));
ReduceVFXIntensity = configFile.Bind("General", "Reduce Visual Effects", false,
new ConfigDescription("Reduce intensity of certain visual effects when you hear the music."));
LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(ReduceVFXIntensity, requiresRestart: false));
Volume = configFile.Bind("General", "Volume", VolumeDefault,
new ConfigDescription("Volume of music played by this mod.", new AcceptableValueRange<float>(VolumeMin, VolumeMax)));
LethalConfigManager.AddConfigItem(new FloatSliderConfigItem(Volume, requiresRestart: false));
@ -2354,8 +2561,9 @@ namespace MuzikaGromche
}
// Create slider entry for track
var seasonal = track.Season is Season season ? $"This is seasonal content for {season.Name}.\n\n" : "";
string warning = track.IsExplicit ? "Explicit Content/Lyrics!\n\n" : "";
string description = $"Language: {language.Full}\n\n{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track.";
string description = $"Language: {language.Full}\n\n{seasonal}{warning}Random (relative) chance of selecting this track.\n\nSet to zero to effectively disable the track.";
track.Weight = configFile.Bind(
new ConfigDefinition(section, track.Name),
50,
@ -2611,7 +2819,7 @@ namespace MuzikaGromche
}
catch (Exception e)
{
Debug.Log($"{nameof(MuzikaGromche)} Unable to parse time series: {e}");
Plugin.Log.LogError($"Unable to parse time series: {e}");
return null;
}
}
@ -2628,7 +2836,7 @@ namespace MuzikaGromche
strings.Append(", ");
}
}
Debug.Log($"{nameof(MuzikaGromche)} format time series {ts} {strings}");
Plugin.Log.LogDebug($"format time series {ts} {strings}");
return strings.ToString();
}
T[]? parseStringArray<T>(string str, Func<string, T> parser, bool sort = false) where T : struct
@ -2641,7 +2849,7 @@ namespace MuzikaGromche
}
catch (Exception e)
{
Debug.Log($"{nameof(MuzikaGromche)} Unable to parse array: {e}");
Plugin.Log.LogError($"Unable to parse array: {e}");
return null;
}
}
@ -2731,12 +2939,12 @@ namespace MuzikaGromche
.FirstOrDefault(prefab => prefab.Prefab.name == JesterEnemyPrefabName);
if (networkPrefab == null)
{
Debug.LogError($"{nameof(MuzikaGromche)} JesterEnemy prefab not found!");
Plugin.Log.LogError("JesterEnemy prefab not found!");
}
else
{
networkPrefab.Prefab.AddComponent<MuzikaGromcheJesterNetworkBehaviour>();
Debug.Log($"{nameof(MuzikaGromche)} Patched JesterEnemy");
Plugin.Log.LogInfo("Patched JesterEnemy");
}
}
}
@ -2761,7 +2969,7 @@ namespace MuzikaGromche
var farAudioTransform = gameObject.transform.Find("FarAudio");
if (farAudioTransform == null)
{
Debug.LogError($"{nameof(MuzikaGromche)} JesterEnemy->FarAudio prefab not found!");
Plugin.Log.LogError("JesterEnemy->FarAudio prefab not found!");
}
else
{
@ -2788,7 +2996,7 @@ namespace MuzikaGromche
Config.Volume.SettingChanged += UpdateVolume;
Debug.Log($"{nameof(MuzikaGromche)} {nameof(MuzikaGromcheJesterNetworkBehaviour)} Patched JesterEnemy");
Plugin.Log.LogInfo($"{nameof(MuzikaGromcheJesterNetworkBehaviour)} Patched JesterEnemy");
}
}
@ -2856,9 +3064,10 @@ namespace MuzikaGromche
[ClientRpc]
public void SetTrackClientRpc(string name)
{
Debug.Log($"{nameof(MuzikaGromche)} SetTrackClientRpc {name}");
Plugin.Log.LogInfo($"SetTrackClientRpc {name}");
if (Plugin.FindTrackNamed(name) is { } track)
{
AudioClipsCacheManager.LoadAudioTrack(track);
CurrentTrack = Config.OverrideCurrentTrack(track);
}
}
@ -2868,7 +3077,7 @@ namespace MuzikaGromche
{
var selectableTrack = Plugin.ChooseTrack();
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
Debug.Log($"{nameof(MuzikaGromche)} ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}");
Plugin.Log.LogInfo($"ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}");
SetTrackClientRpc(audioTrack.Name);
SelectedTrackIndex += 1;
}
@ -2877,7 +3086,7 @@ namespace MuzikaGromche
{
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}");
Plugin.Log.LogDebug($"Play Intro: dspTime={AudioSettings.dspTime:N4}, intro.time={IntroAudioSource.time:N4}/{IntroAudioSource.clip.length:N4}, scheduled Loop={loopStartDspTime}");
}
public void OverrideDeathScreenGameOverText()
@ -2935,10 +3144,21 @@ namespace MuzikaGromche
var introAudioSource = behaviour.IntroAudioSource;
var loopAudioSource = behaviour.LoopAudioSource;
if (behaviour.CurrentTrack == null)
if (behaviour.CurrentTrack == null || behaviour.CurrentTrack.LoadedIntro == null || behaviour.CurrentTrack.LoadedLoop == null)
{
#if DEBUG
Debug.LogError($"{nameof(MuzikaGromche)} CurrentTrack is not set!");
if (behaviour.CurrentTrack == null)
{
Plugin.Log.LogError("CurrentTrack is not set!");
}
else if (AudioClipsCacheManager.AllDone)
{
Plugin.Log.LogError("Failed to load audio clips, no in-flight requests running");
}
else
{
Plugin.Log.LogDebug($"Waiting for audio clips to load");
}
#endif
return;
}

View File

@ -205,7 +205,7 @@ namespace MuzikaGromche
patch.ManualPatch?.Invoke(animationContainer);
animator.runtimeAnimatorController = patch.AnimatorController;
Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller");
Plugin.Log.LogDebug($"{nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller");
}
}
}
@ -223,7 +223,7 @@ namespace MuzikaGromche
#pragma warning restore CS0162 // Unreachable code detected
}
targetObject.name = newName;
Debug.Log($"{nameof(MuzikaGromche)} {nameof(PoweredLightsAnimatorsPatch)} {animatorContainer.name}/{relativePath}: Renamed GameObject");
Plugin.Log.LogDebug($"{nameof(PoweredLightsAnimatorsPatch)} {animatorContainer.name}/{relativePath}: Renamed GameObject");
};
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MuzikaGromche;
public delegate bool SeasonalContentPredicate(DateTime dateTime);
// I'm not really sure what to do with seasonal content yet.
//
// There could be two approaches:
// - Force seasonal content tracks to be the only available tracks during their season.
// Then seasons must be short, so they don't cut off too much content for too long.
// - Exclude seasonal content tracks from the pool when their season is not active.
// Considering how many tracks are there in the playlist permanently already,
// this might not give the seasonal content enough visibility.
//
// Either way, seasonal content tracks would be listed in the config UI at all times,
// which makes it confusing if you try to select only seasonal tracks outside of their season.
// Seasons may NOT overlap. There is at most ONE active season at any given date.
public readonly record struct Season(string Name, string Description, SeasonalContentPredicate IsActive)
{
public override string ToString() => Name;
public static readonly Season NewYear = new("New Year", "New Year and Christmas holiday season", dateTime =>
{
// December 10 - February 29
var month = dateTime.Month;
var day = dateTime.Day;
return (month == 12 && day >= 10) || (month == 1) || (month == 2 && day <= 29);
});
// Note: it is important that this property goes last
public static readonly Season[] All = [NewYear];
}
public interface ISeasonalContent
{
public Season? Season { get; init; }
}
public static class SeasonalContentManager
{
public static Season? CurrentSeason(DateTime dateTime)
{
foreach (var season in Season.All)
{
if (season.IsActive(dateTime))
{
return season;
}
}
return null;
}
public static Season? CurrentSeason() => CurrentSeason(DateTime.Today);
// Take second approach: filter out seasonal content that is not in the current season.
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items, Season? season) where T : ISeasonalContent
{
return items.Where(item =>
{
if (item.Season == null)
{
return true; // always available
}
return item.Season == season;
});
}
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items, DateTime dateTime) where T : ISeasonalContent
{
var season = CurrentSeason(dateTime);
return Filter(items, season);
}
public static IEnumerable<T> Filter<T>(this IEnumerable<T> items) where T : ISeasonalContent
{
return Filter(items, DateTime.Today);
}
}

View File

@ -80,7 +80,7 @@ namespace MuzikaGromche
var multiplier = Mathf.Lerp(minMultiplier, maxMultiplier, normalizedMultiplierTime);
var newWeight = Mathf.FloorToInt(weights[index] * multiplier);
Debug.Log($"{nameof(MuzikaGromche)} {nameof(SpawnRatePatch)} Overriding spawn weight[{index}] {weights[index]} * {multiplier} => {newWeight} for t={SpawnTime}");
Plugin.Log.LogInfo($"{nameof(SpawnRatePatch)} Overriding spawn weight[{index}] {weights[index]} * {multiplier} => {newWeight} for t={SpawnTime}");
weights[index] = newWeight;
}
}

View File

@ -23,12 +23,18 @@ Muzika Gromche v1337.9001.0 has been updated to work with Lethal Company v73. Pr
## Playlist
English playlist features artists such as **Imagine Dragons, Fall Out Boy, Bon Jovi, Black Eyed Peas, LMFAO** (Party Rock Anthem / Every day I'm shufflin'), **CYBEЯIA** / "Cyberia" (Russian Hackers), and of course **Whistle** by Joel Merry / Flo Rida.
English playlist features artists such as **Imagine Dragons, Fall Out Boy, Bon Jovi, Nirvana, Black Eyed Peas, LMFAO** (Party Rock Anthem / Every day I'm shufflin'), **CYBEЯIA** / "Cyberia" (Russian Hackers), **t.A.T.u.**, and of course **Whistle** by Joel Merry / Flo Rida.
Russian playlist includes **Би-2, Витас, ГлюкoZa** (Глюкоза) & **Ленинград, Дискотека Авария, Noize MC, Oxxxymiron, Сплин, Пошлая Молли.**
There are also a K-pop track by **aespa**, an anime opening from **One Punch Man,** and an Indian banger by **CarryMinati & Wily Frenzy.**
Seasonal New Year's songs:
- **My Chemical Romance - All I Want for Christmas Is You** (codenamed **IkWilJe**)
- **Элизиум - Три белых коня** (codenamed **Paarden**)
- **Дискотека Авария - Новогодняя** (codenamed **DiscoKapot**)
## Configuration
Configuration integrates with [`LethalConfig`] mod.
@ -52,6 +58,8 @@ Any player can change the following personal preferences locally.
See also [mod's release thread](https://discord.com/channels/1168655651455639582/1433881654866477318) at [Lethal Company Modding](https://discord.gg/XeyYqRdRGC) Discord server (in case the invite link expires, there should be a fresh one at [lethal.wiki](https://lethal.wiki/)).
Check out my other mod, [HookahPlace ship 'furniture'](https://thunderstore.io/c/lethal-company/p/Ratijas/HookahPlace/)!
---
1. Actually not limited to Inverse teleporter or Titan.

View File

@ -6,7 +6,15 @@
"version": "4.4.2",
"commands": [
"netcode-patch"
]
],
"rollForward": false
},
"tcli": {
"version": "0.2.4",
"commands": [
"tcli"
],
"rollForward": false
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "MuzikaGromche",
"version_number": "1337.9001.2",
"version_number": "1337.9001.4",
"author": "Ratijas",
"description": "Add some content to your inverse teleporter experience on Titan!",
"website_url": "https://git.vilunov.me/ratijas/muzika-gromche",

20
thunderstore.toml Normal file
View File

@ -0,0 +1,20 @@
# - set token variable from .env file
# - dotnet tool restore
# - dotnet tcli publish --file dist/MuzikaGromche-Release.zip
[config]
schemaVersion = "0.0.1"
[package]
namespace = "Ratijas"
name = "MuzikaGromche"
description = "Add some content to your inverse teleporter experience on Titan!"
websiteUrl = "https://git.vilunov.me/ratijas/muzika-gromche"
containsNsfwContent = false
[publish]
repository = "https://thunderstore.io"
communities = [ "lethal-company" ]
[publish.categories]
lethal-company = [ "mods", "audio", "bepinex", "clientside", "serverside", "monsters" ]