1
0
Fork 0

Compare commits

..

No commits in common. "dev" and "master" have entirely different histories.
dev ... master

108 changed files with 341 additions and 5340 deletions

View File

@ -1,15 +0,0 @@
[*.cs]
# IDE0290: Use primary constructor
# Primary constructors are far from perfect: they can't have readonly fields, while fields can be used anywhere in the class body.
csharp_style_prefer_primary_constructors = false
# IDE0305: Simplify collection initialization
dotnet_style_prefer_collection_expression = never
# IDE0031: Use null propagation
# Unity overrides equality operator, so gameObject == null also accounts for internal state of the backing C++ object
# Read more:
# - https://blog.lslabs.dev/posts/null_check_equality_unity
# - https://blog.lslabs.dev/posts/unity_script_duality
dotnet_style_null_propagation = false

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
Assets/DeployDestroyLoop.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/DeployDestroyStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
Assets/DurochkaLoop.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/DurochkaStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
Assets/GorgorodLoop.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/GorgorodStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
Assets/MoyaZhittyaLoop.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/MoyaZhittyaStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/MuzikaGromcheLoop.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/MuzikaGromcheStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
Assets/VseVZaleLoop.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

BIN
Assets/VseVZaleStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -1,128 +0,0 @@
# Changelog
## MuzikaGromche 1337.9001.69
- Show real Artist & Song info in the config.
## MuzikaGromche 1337.9001.68 - LocalHost hotfix
- Fixed occasionally broken playback of v1337.9001.67, sorry about that.
- Turns out, client-side vanilla-compat mode can never be perfectly timed, so don't expect much without a modded host.
- Removed an existing track Yalgaar.
- Merged two config options into one: Reduce Visual Effects & Display Lyrics.
- Added a new track Arcane.
## MuzikaGromche 1337.9001.67 - LocalHost Edition
- Added a new track TwoFastTuFurious (from the same artist as PickUpSticks), thematic to the upcoming Valentine's Day.
- Added support for client-side playback while playing with an unmodded/vanilla host.
- Tweaked the amount of visual flare at the Factory's start room (main tile).
## 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
- Raised the default audio volume, and added a configuration slider.
- Tweaked color palette, lyrics and visual effects for MoyaZhittya and some other tracks.
## MuzikaGromche 1337.9001.0 - v73 Music quieter Edition
- Updated netcode-patch to support Lethal Company v73.
- Remastered all the audio tracks to target a consistent loudness level which allows you hear your teammates.
- Remastered track Song2 to fix cut points.
- Shortened intro of track Peretasovka to match vanilla timings.
- Added multiple intro variants for BeefLiver.
- Added a new track BbIXODaHET.
- Added a new track Whistle. Now it can fully replace WhistleJester!
- Added a new track ReelGoon.
## MuzikaGromche 1337.420.9004 - Life Support Edition
- Override Death Screen / Game Over text in certain cases.
- Added a new track AttentionPls featuring multiple intro variants and new visual effects.
## MuzikaGromche 1337.420.9003 - Lights Out Edition
- Fixed wrong colors during fade out transition, e.g. in Mineshaft tunnel tiles.
## 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.
- Added a new track Beha with three different variants of intro.
## MuzikaGromche 1337.420.69 - It's All DiscoNnected Edition
- Fixed harmless but annoying errors in BepInEx console output.
- Improve smoothness of color animations.
- Added a new track BeefLiver.
## MuzikaGromche 1337.69.420 - It's All Connected Edition
- Fix certain object hanging around after being disabled.
- CSync proved to be unreliable for config syncing, so rewrote track selection to custom netcode.
## MuzikaGromche 13.37.9001 - Chromaberrated Edition
- Fixed more missing flickering behaviours for some animators controllers.
- Fixed some powered lights not fully turning off or flickering when there are multiple Light components per container.
- Improved performance by pre-loading certain assets at the start of round instead of at a timing-critical frame update.
- Added an opt-in config option to increase certain spawn rate to experience content of this mod more often.
## MuzikaGromche 13.37.1337 - Photosensitivity Warning Edition
- Added LobbyCompatibility to dependencies to avoid desync issues.
- Fixed lyrics not being displayed in some situations.
- Fixed visual issues with the fade out effect.
- Fixed visual glitch at the last beat of a loop.
- Fixed timings of one of the tracks.
- Removed unnecessary "Enable Color Animations" config option.
- Fixed missing flickering behaviours for some animators controllers.
## MuzikaGromche 13.37.911 - Sri Lanka Bus hotfix
- Fixed certain event sometimes not working due to wrong method call.
- Added support for pre-v70 Mansion Main tile.
## MuzikaGromche 13.37.420 - Sri Lanka Bus Edition
Completely rewritten by Ratijas, with tons of new content.
- Added lots of new tracks.
- Fixed gaps in old tracks.
- New code synchronizes light show to the beat.
- Timings, animation curves, color palettes and events fine-tuned for each track by visual artist [Just Nothing](https://t.me/REALJUSTNOTHING).
- Configurable Audio Delay for those with Bluetooth headset.
- Configurable chance of randomly choosing each tracks.
- Added lyrics to *some* of the tracks, and a configuration toggle.
- Certain tiles are patched by [WaterGun](https://www.youtube.com/channel/UCCxCFfmrnqkFZ8i9FsXBJVA) to add some visual flare.
## MuzikaGromche 13.37.6 - Christmas Special
Last known version released by Oflor. Added special timed content for New Year and Christmas.

View File

@ -1,5 +0,0 @@
<Project>
<Target Name="NetcodePatch" AfterTargets="PostBuildEvent">
<Exec Command="dotnet netcode-patch -uv 2022.3.62 -nv 1.12.0 &quot;$(TargetPath)&quot; @(ReferencePathWithRefAssemblies->'&quot;%(Identity)&quot;', ' ')"/>
</Target>
</Project>

View File

@ -11,7 +11,7 @@ build-debug:
clean:
rm -rf dist MuzikaGromche/bin MuzikaGromche/obj
plugin_dir := "$HOME/.config/r2modmanPlus-local/LethalCompany/profiles" / imperium_profile / "BepInEx/plugins/Ratijas-MuzikaGromche/"
plugin_dir := "$HOME/.config/r2modmanPlus-local/LethalCompany/profiles" / imperium_profile / "BepInEx/plugins/Oflor-MuzikaGromche/"
install-imperium:
rm -rf "{{ plugin_dir }}"
@ -26,9 +26,3 @@ bump version_number:
jq --indent 4 --arg v "{{ version_number }}" '.version_number = $v' < manifest.json > manifest.json.copy
mv manifest.json.copy manifest.json
sed -i 's/<Version>.*<\/Version>/<Version>{{ version_number }}<\/Version>/' MuzikaGromche/MuzikaGromche.csproj
ogg track_name:
dotnet msbuild /t:wav2ogg /p:TrackName="{{ track_name }}"
ogg1 track_name:
dotnet msbuild /t:wav2ogg1 /p:TrackName="{{ track_name }}"

16
MuzikaGromche.sln Normal file
View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuzikaGromche", "MuzikaGromche\MuzikaGromche.csproj", "{72633315-F098-4E09-B32B-9224376CD9A5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{72633315-F098-4E09-B32B-9224376CD9A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72633315-F098-4E09-B32B-9224376CD9A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72633315-F098-4E09-B32B-9224376CD9A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72633315-F098-4E09-B32B-9224376CD9A5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,3 +0,0 @@
<Solution>
<Project Path="MuzikaGromche/MuzikaGromche.csproj" />
</Solution>

View File

@ -2,13 +2,9 @@
<Project>
<PropertyGroup>
<!-- Copy this file to MuzikaGromche.props.user and uncomment one of two paths below: -->
<!-- On Linux: -->
<!-- <LethalCompanyDir>$(HOME)/.local/share/Steam/steamapps/common/Lethal Company/</LethalCompanyDir> -->
<!-- <WavExportDir>\home\ratijas\Music\SFX\Export</WavExportDir> -->
<!-- On Windows: -->
<!-- <LethalCompanyDir>C:/Program Files (x86)/Steam/steamapps/common/Lethal Company/</LethalCompanyDir> -->
<!-- <WavExportDir>D:\Code\MuzikaGromcheAudio\Export</WavExportDir> -->
</PropertyGroup>
</Project>

View File

@ -1,170 +0,0 @@
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

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

View File

@ -1,72 +0,0 @@
using System.Collections;
using HarmonyLib;
using TMPro;
using UnityEngine;
namespace MuzikaGromche
{
static class DeathScreenGameOverTextManager
{
private const string GameOverTextVanilla = "[LIFE SUPPORT: OFFLINE]";
public const string GameOverTextModdedDefault = "[ MUZIKA: GROMCHE ]";
public static void Clear()
{
SetTextImpl(GameOverTextVanilla);
}
public static void SetText(string? text)
{
SetTextImpl(text ?? GameOverTextModdedDefault);
}
public static IEnumerator SetTextAndClear(string? text)
{
SetText(text);
// Game Over animation duration is about 4.25 seconds
yield return new WaitForSeconds(5f);
Clear();
}
private static void SetTextImpl(string text)
{
GameObject textGameObject = GameObject.Find("Systems/UI/Canvas/DeathScreen/GameOverText");
if (textGameObject == null)
{
return;
}
TextMeshProUGUI tmp = textGameObject.GetComponent<TextMeshProUGUI>();
if (tmp == null)
{
return;
}
var transform = textGameObject.GetComponent<RectTransform>();
if (transform == null)
{
return;
}
// Default transform width is 645.8 which only fit the default message and no extra characters
Vector2 size = transform.sizeDelta;
if (Mathf.Approximately(size.x, 645.8f))
{
size.x += 100f;
transform.sizeDelta = size;
}
tmp.text = text;
}
}
[HarmonyPatch(typeof(RoundManager))]
static class DeathScreenGameOverTextResetPatch
{
[HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
[HarmonyPrefix]
static void OnDestroy(RoundManager __instance)
{
var _ = __instance;
DeathScreenGameOverTextManager.Clear();
}
}
}

View File

@ -1,70 +0,0 @@
using BepInEx.Logging;
namespace MuzikaGromche;
#if DEBUG
// A logger with an API similar to ManualLogSource.
// This logger caches last posted messagee and uses it to deduplicate subsequent messages.
// Use Clear() to forget deduplicated message.
internal class DedupManualLogSource
{
public ManualLogSource Source;
object? lastData = null;
public DedupManualLogSource(ManualLogSource source)
{
Source = source;
}
public void Log(LogLevel level, object data)
{
if (lastData != data)
{
lastData = data;
Source.Log(level, data);
}
}
public void LogFatal(object data)
{
Log(LogLevel.Fatal, data);
}
public void LogError(object data)
{
Log(LogLevel.Error, data);
}
public void LogWarning(object data)
{
Log(LogLevel.Warning, data);
}
public void LogMessage(object data)
{
Log(LogLevel.Message, data);
}
public void LogInfo(object data)
{
Log(LogLevel.Info, data);
}
public void LogDebug(object data)
{
Log(LogLevel.Debug, data);
}
public void Clear()
{
lastData = null;
}
public void Dispose()
{
}
}
#endif

View File

@ -1,165 +0,0 @@
using DunGen;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
namespace MuzikaGromche
{
public static class DiscoBallManager
{
// A struct holding a disco ball container object and the name of a tile for which it was designed.
private readonly record struct TilePatch(string TileName, GameObject DiscoBallContainer)
{
// We are specifically looking for cloned tiles, not the original prototypes.
public readonly string TileCloneName = $"{TileName}(Clone)";
}
private static TilePatch[] Patches = [];
private static readonly List<GameObject> CachedDiscoBalls = [];
private static readonly List<Animator> CachedDiscoBallAnimators = [];
private static readonly string[] AnimatorContainersNames = [
"DiscoBallProp/AnimContainer",
"DiscoBallProp1/AnimContainer",
"DiscoBallProp2/AnimContainer",
"DiscoBallProp3/AnimContainer",
"DiscoBallProp4/AnimContainer",
"DiscoBallProp5/AnimContainer",
];
public static void Load()
{
const string BundleFileName = "muzikagromche_discoball";
string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleFileName);
var assetBundle = AssetBundle.LoadFromFile(bundlePath)
?? throw new NullReferenceException("Failed to load bundle");
(string PrefabPath, string TileName)[] patchDescriptors =
[
("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManor.prefab", "ManorStartRoomSmall"),
("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerManorOLD.prefab", "ManorStartRoom"),
("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerFactory.prefab", "StartRoom"),
("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerMineShaft.prefab", "MineshaftStartTile"),
("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerLargeForkTileB.prefab", "LargeForkTileB"),
("Assets/LethalCompany/Mods/MuzikaGromche/DiscoBallContainerBirthdayRoomTile.prefab", "BirthdayRoomTile"),
];
Patches = [.. patchDescriptors.Select(d =>
new TilePatch(d.TileName, assetBundle.LoadAsset<GameObject>(d.PrefabPath))
)];
}
internal static void Patch(Tile tile)
{
var query = from patch in Patches
where tile.gameObject.name == patch.TileCloneName
select patch;
// Should be just one, but FirstOrDefault() isn't usable with structs
foreach (var patch in query)
{
Patch(tile, patch);
}
}
static void Patch(Tile tile, TilePatch patch)
{
var discoBall = UnityEngine.Object.Instantiate(patch.DiscoBallContainer, tile.transform);
if (discoBall == null)
{
return;
}
foreach (var animator in FindDiscoBallAnimators(discoBall))
{
CachedDiscoBallAnimators.Add(animator);
}
CachedDiscoBalls.Add(discoBall);
discoBall.SetActive(false);
Plugin.Log.LogDebug($"{nameof(DiscoBallManager)} Patched tile '{tile.gameObject.name}'");
}
static IEnumerable<Animator> FindDiscoBallAnimators(GameObject discoBall)
{
foreach (var animatorContainerName in AnimatorContainersNames)
{
var transform = discoBall.transform.Find(animatorContainerName);
if (transform == null)
{
// Not all prefabs have all possible animators, and it's OK
continue;
}
var animator = transform.gameObject?.GetComponent<Animator>();
if (animator == null)
{
// This would be weird
continue;
}
yield return animator;
}
}
public static void Toggle(bool on)
{
Plugin.Log.LogDebug($"{nameof(DiscoBallManager)} Toggle {(on ? "ON" : "OFF")} {CachedDiscoBallAnimators.Count} animators");
foreach (var discoBall in CachedDiscoBalls)
{
discoBall.SetActive(on);
}
foreach (var animator in CachedDiscoBallAnimators)
{
animator?.SetBool("on", on);
}
}
public static void Enable()
{
Toggle(true);
}
public static void Disable()
{
Toggle(false);
}
internal static void Clear()
{
Plugin.Log.LogDebug($"{nameof(DiscoBallManager)} Clearing {CachedDiscoBalls.Count} disco balls & {CachedDiscoBallAnimators.Count} animators");
CachedDiscoBallAnimators.Clear();
CachedDiscoBalls.Clear();
}
}
[HarmonyPatch(typeof(Tile))]
static class DiscoBallTilePatch
{
[HarmonyPatch(nameof(Tile.AddTriggerVolume))]
[HarmonyPostfix]
static void OnAddTriggerVolume(Tile __instance)
{
DiscoBallManager.Patch(__instance);
}
}
[HarmonyPatch(typeof(RoundManager))]
static class DiscoBallDespawnPatch
{
[HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
[HarmonyPrefix]
static void OnDestroy(RoundManager __instance)
{
var _ = __instance;
DiscoBallManager.Clear();
}
}
}

View File

@ -1,101 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using UnityEngine;
namespace MuzikaGromche
{
#if DEBUG
// Dumps list of tracks as a JSON on launch.
static class Exporter
{
public static void ExportTracksJSON(ISelectableTrack[] tracks)
{
// Same directory where save files are located:
// C:\Users\user\AppData\LocalLow\ZeekerssRBLX\Lethal Company\
string directory = Application.persistentDataPath;
string fileName = "MuzikaGromcheTracks.json";
string filePath = Path.Join(directory, fileName);
var jsonObject = new Dictionary<string, object>();
var tracksList = new List<object>();
jsonObject["version"] = MyPluginInfo.PLUGIN_VERSION;
jsonObject["tracks"] = tracksList;
foreach (var (selectableTrack, audioTrack) in SelectTracks(tracks))
{
tracksList.Add(SerializeTrack(selectableTrack, audioTrack));
}
using StreamWriter sw = new(filePath);
using JsonWriter writer = new JsonTextWriter(sw)
{
Formatting = Formatting.Indented,
};
JsonSerializer serializer = new()
{
NullValueHandling = NullValueHandling.Include,
};
serializer.Serialize(writer, jsonObject);
}
private static IEnumerable<(ISelectableTrack selectableTrack, IAudioTrack audioTrack)> SelectTracks(ISelectableTrack[] tracks)
{
foreach (var selectableTrack in tracks)
{
foreach (var audioTrack in selectableTrack.GetTracks())
{
yield return (selectableTrack, audioTrack);
}
}
}
private static Dictionary<string, object?> SerializeTrack(ISelectableTrack selectableTrack, IAudioTrack audioTrack)
{
var obj = new Dictionary<string, object?>
{
["Enabled"] = selectableTrack.Enabled,
["Name"] = audioTrack.Name, // may be different from selectableTrack.Name, if selectable track is a group
["Artist"] = selectableTrack.Artist,
["Song"] = selectableTrack.Song,
["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 ?? 0f,
["FileDurationLoop"] = audioTrack.LoadedLoop?.length ?? 0f,
["FileNameIntro"] = audioTrack.FileNameIntro,
["FileNameLoop"] = audioTrack.FileNameLoop,
["BeatsOffset"] = audioTrack.BeatsOffset,
["FadeOutBeat"] = audioTrack.FadeOutBeat,
["FadeOutDuration"] = audioTrack.FadeOutDuration,
["ColorTransitionIn"] = audioTrack.ColorTransitionIn,
["ColorTransitionOut"] = audioTrack.ColorTransitionOut,
["ColorTransitionEasing"] = audioTrack.ColorTransitionEasing.Name,
["FlickerLightsTimeSeries"] = audioTrack.FlickerLightsTimeSeries,
["Lyrics"] = SerializeTimeSeries(new TimeSeries<string>(audioTrack.LyricsTimeSeries, audioTrack.LyricsLines)),
["DrunknessLoopOffsetTimeSeries"] = SerializeTimeSeries(audioTrack.DrunknessLoopOffsetTimeSeries),
["CondensationLoopOffsetTimeSeries"] = SerializeTimeSeries(audioTrack.CondensationLoopOffsetTimeSeries),
["Palette"] = audioTrack.Palette.Colors.Select(SerializeColor).ToList(),
["GameOverText"] = audioTrack.GameOverText,
};
return obj;
}
private static object SerializeTimeSeries<T>(TimeSeries<T> timeSeries)
{
return timeSeries.Beats.Zip(timeSeries.Values, (one, two) => new object?[] { one, two }).ToList();
}
private static string SerializeColor(Color color)
{
string colorHex = $"#{ColorUtility.ToHtmlStringRGB(color)}";
return colorHex;
}
}
#endif
}

View File

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

View File

@ -1,30 +0,0 @@
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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,79 +2,41 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Title>MuzikaGromche</Title>
<PackageId>MuzikaGromche</PackageId>
<RootNamespace>MuzikaGromche</RootNamespace>
<AssemblyName>Ratijas.MuzikaGromche</AssemblyName>
<Product>Muzika Gromche</Product>
<Description>Add some content to your inverse teleporter experience on Titan!</Description>
<Version>1337.9001.69</Version>
<AssemblyName>MuzikaGromche</AssemblyName>
<Description>Opa che tut u nas</Description>
<Version>13.37.6</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<PackageReadmeFile>../README.md</PackageReadmeFile>
<PackageProjectUrl>https://git.vilunov.me/ratijas/muzika-gromche</PackageProjectUrl>
<RepositoryUrl>https://git.vilunov.me/ratijas/muzika-gromche</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- NuGet Information -->
<RestoreAdditionalProjectSources>
https://api.nuget.org/v3/index.json;
https://nuget.bepinex.dev/v3/index.json;
https://nuget.windows10ce.com/nuget/v3/index.json
</RestoreAdditionalProjectSources>
<!-- Prevent Publicizer Warnings from Showing -->
<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="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" />
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all"/>
<PackageReference Include="BepInEx.Core" Version="5.*"/>
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*"/>
<PackageReference Include="UnityEngine.Modules" Version="2022.3.9" IncludeAssets="compile"/>
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" />
<!--
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
-->
<PackageReference Include="Sigurd.BepInEx.CSync" Version="5.0.1" Publicize="true" />
<PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" />
</ItemGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" Publicize="true" Private="false">
<Reference Include="Assembly-CSharp" Publicize="true">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Unity.Collections" Private="false">
<Reference Include="Unity.Collections">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath>
</Reference>
<Reference Include="Unity.Netcode.Runtime" Publicize="true" Private="false">
<Reference Include="Unity.Netcode.Runtime" Publicize="true">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll</HintPath>
</Reference>
<Reference Include="Unity.TextMeshPro" Publicize="true" Private="false">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.TextMeshPro.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UI" Publicize="true" Private="false">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\UnityEngine.UI.dll</HintPath>
</Reference>
<Reference Include="Unity.RenderPipelines.Core.Runtime" Publicize="true" Private="false">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.RenderPipelines.Core.Runtime.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json" Publicize="false" Private="false">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" Private="false" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all"/>
</ItemGroup>
<Target Name="Bundle" AfterTargets="Build">
@ -85,12 +47,9 @@
<ItemGroup>
<PackagedResources Include="$(SolutionDir)README.md" />
<PackagedResources Include="$(SolutionDir)CHANGELOG.md" />
<PackagedResources Include="$(SolutionDir)icon.png" />
<PackagedResources Include="$(SolutionDir)manifest.json" />
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
<PackagedResources Include="$(TargetDir)MuzikaGromche.dll" />
</ItemGroup>
<ItemGroup>
@ -114,25 +73,4 @@
DestinationFolder="$(SolutionDir)dist\"
/>
</Target>
<!--
Usage:
Set WavExportDir in props.user file.
Run
> dotnet msbuild /t:wav2ogg /p:TrackName=GodMode
-->
<Target Name="wav2ogg">
<ItemGroup>
<TrackNames Include="$(TrackName)Intro" />
<TrackNames Include="$(TrackName)Loop" />
</ItemGroup>
<Exec Command="ffmpeg -bitexact -y -i $(WavExportDir)%(TrackNames.Identity).wav $(SolutionDir)Assets\%(TrackNames.Identity).ogg" />
</Target>
<Target Name="wav2ogg1">
<ItemGroup>
<TrackNames Include="$(TrackName)" />
</ItemGroup>
<Exec Command="ffmpeg -bitexact -y -i $(WavExportDir)%(TrackNames.Identity).wav $(SolutionDir)Assets\%(TrackNames.Identity).ogg" />
</Target>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -1,346 +0,0 @@
using DunGen;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
namespace MuzikaGromche
{
static class PoweredLightsAnimators
{
private const string PoweredLightTag = "PoweredLight";
private delegate void ManualPatch(GameObject animatorContainer);
private readonly record struct AnimatorPatch(
string AnimatorContainerPath,
RuntimeAnimatorController AnimatorController,
bool AddTagAndAnimator,
ManualPatch? ManualPatch);
private readonly record struct TilePatch(string TileName, AnimatorPatch[] Patches)
{
// We are specifically looking for cloned tiles, not the original prototypes.
public readonly string TileCloneName = $"{TileName}(Clone)";
}
private readonly record struct AnimatorPatchDescriptor(
string AnimatorContainerPath,
string AnimatorControllerAssetPath,
bool AddTagAndAnimator = false,
ManualPatch? ManualPatch = null)
{
public AnimatorPatch Load(AssetBundle assetBundle)
{
var animationController = assetBundle.LoadAsset<RuntimeAnimatorController>(AnimatorControllerAssetPath)
?? throw new FileNotFoundException($"RuntimeAnimatorController not found: {AnimatorControllerAssetPath}", AnimatorControllerAssetPath);
return new(AnimatorContainerPath, animationController, AddTagAndAnimator, ManualPatch);
}
}
private readonly record struct TilePatchDescriptor(string[] TileNames, AnimatorPatchDescriptor[] Descriptors)
{
public TilePatchDescriptor(string TileName, AnimatorPatchDescriptor[] Descriptors)
: this([TileName], Descriptors)
{
}
public TilePatch[] Load(AssetBundle assetBundle)
{
var patches = Descriptors.Select(d => d.Load(assetBundle)).ToArray();
return [.. TileNames.Select(tileName => new TilePatch(tileName, patches))];
}
}
private static IDictionary<string, TilePatch> Patches = new Dictionary<string, TilePatch>();
private static AudioClip AudioClipOn = null!;
private static AudioClip AudioClipOff = null!;
private static AudioClip AudioClipFlicker = null!;
public static void Load()
{
const string BundleFileName = "muzikagromche_poweredlightsanimators";
string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleFileName);
var assetBundle = AssetBundle.LoadFromFile(bundlePath)
?? throw new NullReferenceException("Failed to load bundle");
AudioClipOn = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOn.ogg");
AudioClipOff = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOff.ogg");
AudioClipFlicker = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightFlicker.ogg");
const string BasePath = "Assets/LethalCompany/Mods/MuzikaGromche/AnimatorControllers/";
const string PointLight4 = $"{BasePath}Point Light (4) (Patched).controller";
const string MineshaftSpotlight = $"{BasePath}MineshaftSpotlight (Patched).controller";
const string LightbulbsLine = $"{BasePath}lightbulbsLineMesh (Patched).controller";
const string CeilingFan = $"{BasePath}CeilingFan (originally GameObject) (Patched).controller";
const string LEDHangingLight = $"{BasePath}LEDHangingLight (Patched).controller";
const string MineshaftStartTileSpotlight = $"{BasePath}MineshaftStartTileSpotlight (New).controller";
TilePatchDescriptor[] descriptors =
[
// any version
new("KitchenTile", [
new("PoweredLightTypeB", PointLight4),
new("PoweredLightTypeB (1)", PointLight4),
]),
// < v70
new("ManorStartRoom", [
new("ManorStartRoom/Chandelier/PoweredLightTypeB (1)", PointLight4),
new("ManorStartRoom/Chandelier2/PoweredLightTypeB", PointLight4),
]),
// v70+
new("ManorStartRoomSmall", [
new("ManorStartRoomMesh/Chandelier/PoweredLightTypeB (1)", PointLight4),
new("ManorStartRoomMesh/Chandelier2/PoweredLightTypeB", PointLight4),
]),
new("NarrowHallwayTile2x2", [
new("MineshaftSpotlight (1)", MineshaftSpotlight),
new("MineshaftSpotlight (2)", MineshaftSpotlight),
]),
new("BirthdayRoomTile", [
new("Lights/MineshaftSpotlight", MineshaftSpotlight),
]),
new("BathroomTileContainer", [
new("MineshaftSpotlight", MineshaftSpotlight),
new("LightbulbLine/lightbulbsLineMesh", LightbulbsLine),
]),
new(["BedroomTile", "BedroomTileB"], [
new("CeilingFanAnimContainer", CeilingFan),
new("MineshaftSpotlight (1)", MineshaftSpotlight),
]),
new("GarageTile", [
new("HangingLEDBarLight (3)", LEDHangingLight),
new("HangingLEDBarLight (4)", LEDHangingLight),
// This HangingLEDBarLight's IndirectLight is wrongly named, so animator couldn't find it
// renamed by WaterGun-V70PoweredLights_Fix-1.1.0
// ManualPatch: RenameGameObjectPatch("IndirectLight (1)", "IndirectLight")),
]),
new("PoolTile", [
new("PoolLights/HangingLEDBarLight", LEDHangingLight),
new("PoolLights/HangingLEDBarLight (4)", LEDHangingLight),
new("PoolLights/HangingLEDBarLight (5)", LEDHangingLight),
]),
new("MineshaftStartTile", [
new("Cylinder.001 (1)", MineshaftStartTileSpotlight, AddTagAndAnimator: true),
new("Cylinder.001 (2)", MineshaftStartTileSpotlight, AddTagAndAnimator: true),
]),
];
Patches = descriptors
.SelectMany(d => d.Load(assetBundle))
.ToDictionary(d => d.TileCloneName, d => d);
}
public static void Patch(Tile tile)
{
if (tile == null)
{
throw new ArgumentNullException(nameof(tile));
}
if (Patches.TryGetValue(tile.gameObject.name, out var tilePatch))
{
foreach (var patch in tilePatch.Patches)
{
Transform animationContainerTransform = tile.gameObject.transform.Find(patch.AnimatorContainerPath);
if (animationContainerTransform == null)
{
#if DEBUG
throw new NullReferenceException($"{tilePatch.TileName}/{patch.AnimatorContainerPath} Animation Container not found");
#endif
#pragma warning disable CS0162 // Unreachable code detected
continue;
#pragma warning restore CS0162 // Unreachable code detected
}
GameObject animationContainer = animationContainerTransform.gameObject;
Animator animator = animationContainer.GetComponent<Animator>();
if (patch.AddTagAndAnimator)
{
animationContainer.tag = PoweredLightTag;
if (animator == null)
{
animator = animationContainer.AddComponent<Animator>();
}
if (!animationContainer.TryGetComponent<PlayAudioAnimationEvent>(out var audioScript))
{
audioScript = animationContainer.AddComponent<PlayAudioAnimationEvent>();
audioScript.audioClip = AudioClipOn;
audioScript.audioClip2 = AudioClipOff;
audioScript.audioClip3 = AudioClipFlicker;
}
if (!animationContainer.TryGetComponent<AudioSource>(out var audioSource))
{
// Copy from an existing AudioSource of another light animator
var otherSource = tile.gameObject.GetComponentInChildren<AudioSource>();
if (otherSource != null)
{
audioSource = animationContainer.AddComponent<AudioSource>();
audioSource.spatialBlend = 1;
audioSource.playOnAwake = false;
audioSource.outputAudioMixerGroup = otherSource.outputAudioMixerGroup;
audioSource.spread = otherSource.spread;
audioSource.rolloffMode = otherSource.rolloffMode;
audioSource.maxDistance = otherSource.maxDistance;
audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, otherSource.GetCustomCurve(AudioSourceCurveType.CustomRolloff));
}
}
}
if (animator == null)
{
#if DEBUG
throw new NullReferenceException($"{tilePatch.TileName}/{patch.AnimatorContainerPath} Animation Component not found");
#endif
#pragma warning disable CS0162 // Unreachable code detected
continue;
#pragma warning restore CS0162 // Unreachable code detected
}
patch.ManualPatch?.Invoke(animationContainer);
animator.runtimeAnimatorController = patch.AnimatorController;
Plugin.Log.LogDebug($"{nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller");
}
}
}
private static ManualPatch RenameGameObjectPatch(string relativePath, string newName) => animatorContainer =>
{
var targetObject = animatorContainer.transform.Find(relativePath)?.gameObject;
if (targetObject == null)
{
#if DEBUG
throw new NullReferenceException($"{animatorContainer.name}/{relativePath}: GameObject not found!");
#endif
#pragma warning disable CS0162 // Unreachable code detected
return;
#pragma warning restore CS0162 // Unreachable code detected
}
targetObject.name = newName;
Plugin.Log.LogDebug($"{nameof(PoweredLightsAnimatorsPatch)} {animatorContainer.name}/{relativePath}: Renamed GameObject");
};
}
[HarmonyPatch(typeof(Tile))]
static class PoweredLightsAnimatorsPatch
{
[HarmonyPatch(nameof(Tile.AddTriggerVolume))]
[HarmonyPostfix]
static void OnAddTriggerVolume(Tile __instance)
{
PoweredLightsAnimators.Patch(__instance);
}
}
[HarmonyPatch(typeof(RoundManager))]
static class AllPoweredLightsPatch
{
// Vanilla method assumes that GameObjects with tag "PoweredLight" only contain a single Light component each.
// This is, however, not true for certains double-light setups, such as:
// - double PointLight (even though one of them is 'Point' another is 'Spot') inside CeilingFanAnimContainer in BedroomTile/BedroomTileB;
// - MineshaftSpotlight when it has not only `Point Light` but also `IndirectLight` in BirthdayRoomTile;
// - (maybe more?)
// In order to fix that, replace singular GetComponentInChildren<Light> with plural GetComponentsInChildren<Light> version.
[HarmonyPatch(nameof(RoundManager.RefreshLightsList))]
[HarmonyPrefix]
static bool OnRefreshLightsList(RoundManager __instance)
{
RefreshLightsListPatched(__instance);
var behaviour = __instance.gameObject.GetComponent<PoweredLightsBehaviour>() ?? __instance.gameObject.AddComponent<PoweredLightsBehaviour>();
behaviour.Refresh();
// Skip the original method
return false;
}
static void RefreshLightsListPatched(RoundManager self)
{
// Reusable list to reduce allocations
List<Light> lights = [];
self.allPoweredLights.Clear();
self.allPoweredLightsAnimators.Clear();
GameObject[] gameObjects = GameObject.FindGameObjectsWithTag("PoweredLight");
if (gameObjects == null)
{
return;
}
foreach (var gameObject in gameObjects)
{
Animator animator = gameObject.GetComponentInChildren<Animator>();
if (!(animator == null))
{
self.allPoweredLightsAnimators.Add(animator);
// Patched section: Use list instead of singular GetComponentInChildren<Light>
gameObject.GetComponentsInChildren(includeInactive: true, lights);
self.allPoweredLights.AddRange(lights);
}
}
foreach (var animator in self.allPoweredLightsAnimators)
{
animator.SetFloat("flickerSpeed", UnityEngine.Random.Range(0.6f, 1.4f));
}
}
}
internal class PoweredLightsBehaviour : MonoBehaviour
{
private struct LightData
{
public Light Light;
public Color InitialColor;
}
private readonly List<LightData> AllPoweredLights = [];
public static PoweredLightsBehaviour Instance { get; private set; } = null!;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
}
public void Refresh()
{
AllPoweredLights.Clear();
foreach (var light in RoundManager.Instance.allPoweredLights)
{
AllPoweredLights.Add(new LightData
{
Light = light,
InitialColor = light.color,
});
}
}
public void SetLightColor(SetLightsColorEvent e)
{
foreach (var data in AllPoweredLights)
{
var color = e.GetColor(data.InitialColor);
data.Light.color = color;
}
}
public void ResetLightColor()
{
foreach (var data in AllPoweredLights)
{
data.Light.color = data.InitialColor;
}
}
}
}

View File

@ -1,146 +0,0 @@
using System.Collections;
using HarmonyLib;
using UnityEngine;
namespace MuzikaGromche
{
static class ScreenFiltersManager
{
private const float VibilityThreshold = 0.01f;
private static bool drunknessChangedThisFrame = false;
private static float drunkness = 0f;
public static float Drunkness
{
get => drunkness;
set
{
drunkness = value;
drunknessChangedThisFrame = true;
ScheduleUpdate();
}
}
private static bool helmetCondensationDropsChangedThisFrame = false;
private static float helmetCondensationDrops = 0f;
public static float HelmetCondensationDrops
{
get => helmetCondensationDrops;
set
{
helmetCondensationDrops = value;
helmetCondensationDropsChangedThisFrame = true;
ScheduleUpdate();
}
}
private static Coroutine? scheduledUpdate = null;
private static void ScheduleUpdate()
{
CancelScheduledUpdate();
scheduledUpdate = HUDManager.Instance.StartCoroutine(ScheduledUpdate());
}
private static void CancelScheduledUpdate()
{
if (scheduledUpdate != null)
{
HUDManager.Instance.StopCoroutine(scheduledUpdate);
scheduledUpdate = null;
}
}
private static IEnumerator ScheduledUpdate()
{
try
{
yield return new WaitForEndOfFrame();
Update();
}
finally
{
scheduledUpdate = null;
}
}
private static void Update()
{
CancelScheduledUpdate();
var hud = HUDManager.Instance;
if (!drunknessChangedThisFrame)
{
// animated roll-off
drunkness = Mathf.Clamp(drunkness - Time.deltaTime / 2f, 0f, 1f);
}
drunknessChangedThisFrame = false;
if (drunkness > VibilityThreshold)
{
var moddedDrunknessFilterWeight = StartOfRound.Instance.drunknessSideEffect.Evaluate(drunkness);
var moddedGasImageAlphaAlpha = moddedDrunknessFilterWeight * 1.5f;
// set the final value to the greatest of the two, so that we don't accidentally undo TZP's visual effect.
hud.drunknessFilter.weight = Mathf.Max(hud.drunknessFilter.weight, moddedDrunknessFilterWeight);
hud.gasImageAlpha.alpha = Mathf.Max(hud.gasImageAlpha.alpha, moddedGasImageAlphaAlpha);
// Image alpha only makes sense if the animator is running
hud.gasHelmetAnimator.SetBool("gasEmitting", value: true);
}
else
{
ClearDrunkness();
}
if (!helmetCondensationDropsChangedThisFrame)
{
// animated roll-off
helmetCondensationDrops = Mathf.Clamp(helmetCondensationDrops - Time.deltaTime / 6f, 0f, 1f);
}
helmetCondensationDropsChangedThisFrame = false;
if (helmetCondensationDrops > VibilityThreshold)
{
// HelmetCondensationDrops
Color color = hud.helmetCondensationMaterial.color;
// set the final value to the greatest of the two, so that we don't accidentally undo steam's visual effect.
color.a = Mathf.Clamp(Mathf.Max(color.a, helmetCondensationDrops), 0f, 0.27f);
hud.helmetCondensationMaterial.color = color;
}
else
{
ClearCondensation();
}
}
public static void Clear()
{
ClearDrunkness();
ClearCondensation();
}
private static void ClearDrunkness()
{
drunkness = 0f;
// Only the stop animation if vanilla doesn't animate TZP right now.
if (GameNetworkManager.Instance.localPlayerController.drunkness == 0f)
{
HUDManager.Instance.gasHelmetAnimator.SetBool("gasEmitting", value: false);
}
// Vanilla will set drunknessFilter.weight and gasImageAlpha.alpha on the next Update anyway.
}
private static void ClearCondensation()
{
helmetCondensationDrops = 0f;
}
[HarmonyPatch(typeof(HUDManager))]
internal static class HUDManagerScreenFiltersPatch
{
[HarmonyPatch(nameof(HUDManager.SetScreenFilters))]
[HarmonyPostfix]
static void SetScreenFiltersPostfix(HUDManager __instance)
{
Update();
}
}
}
}

View File

@ -1,82 +0,0 @@
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

@ -1,88 +0,0 @@
using HarmonyLib;
using System;
using UnityEngine;
namespace MuzikaGromche
{
[HarmonyPatch(typeof(RoundManager))]
static class SpawnRatePatch
{
const string JesterEnemyName = "Jester";
// GetRandomWeightedIndex is not only called from AssignRandomEnemyToVent,
// so in order to differentiate it from other calls, prefix assigns these
// global variables, and postfix cleans them up.
// If set to null, do not override spawn chances.
// Otherwise, it is an index of Jester in RoundManager.currentLevel.Enemies
static int? EnemyIndex = null;
static float SpawnTime = 0f;
[HarmonyPatch(nameof(RoundManager.AssignRandomEnemyToVent))]
[HarmonyPrefix]
static void AssignRandomEnemyToVentPrefix(RoundManager __instance, EnemyVent vent, float spawnTime)
{
if (!Config.OverrideSpawnRates.Value)
{
return;
}
var index = __instance.currentLevel.Enemies.FindIndex(enemy => enemy.enemyType.enemyName == JesterEnemyName);
if (index == -1)
{
return;
}
EnemyIndex = index;
SpawnTime = spawnTime;
}
[HarmonyPatch(nameof(RoundManager.AssignRandomEnemyToVent))]
[HarmonyPostfix]
static void AssignRandomEnemyToVentPostfix(RoundManager __instance, EnemyVent vent, float spawnTime)
{
EnemyIndex = null;
SpawnTime = 0f;
}
[HarmonyPatch(nameof(RoundManager.GetRandomWeightedIndex))]
[HarmonyPrefix]
static void GetRandomWeightedIndexPostfix(RoundManager __instance, int[] weights, System.Random randomSeed)
{
if (EnemyIndex is int index)
{
if (__instance.EnemyCannotBeSpawned(index))
{
return;
}
// 0 == 6:00 AM
// 60 == 7:00 AM
// 100 == 7:40 AM (Cycle #1)
// 120 == 8:00 AM
// 180 == 9:00 AM (Cycle #2)
// 300 == 11:00 AM (Cycle #3)
// 420 == 1:00 PM (Cycle #4)
// 540 == 3:00 PM (Cycle #5)
// 660 ~= 5:00 PM
// 780 ~= 7:00 PM
// 900 ~= 9:00 PM
// 1020 ~= 11:00 PM
// 1080 == 12:00 AM
const float minMultiplierTime = 200f; // 9:20 AM
const float maxMultiplierTime = 500f; // 2:00 PM
var normalizedMultiplierTime = Mathf.Clamp((SpawnTime - minMultiplierTime) / (maxMultiplierTime - minMultiplierTime), 0f, 1f);
// Start slowly, then escalate it quickly
normalizedMultiplierTime = Easing.InCubic.Eval(normalizedMultiplierTime);
const float minMultiplier = 1f;
const float maxMultiplier = 15f;
var multiplier = Mathf.Lerp(minMultiplier, maxMultiplier, normalizedMultiplierTime);
var newWeight = Mathf.FloorToInt(weights[index] * multiplier);
Plugin.Log.LogInfo($"{nameof(SpawnRatePatch)} Overriding spawn weight[{index}] {weights[index]} * {multiplier} => {newWeight} for t={SpawnTime}");
weights[index] = newWeight;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More