Compare commits
206 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
8942b2ee37 | |
|
|
f20a500992 | |
|
|
6a71ed578b | |
|
|
3490b1c2f3 | |
|
|
cbf1c14e01 | |
|
|
5258b806d4 | |
|
|
4cc187b525 | |
|
|
ecee2a25e2 | |
|
|
af38056d11 | |
|
|
567597e353 | |
|
|
ee2b1574d0 | |
|
|
02903ba537 | |
|
|
b0d5922c3c | |
|
|
4e9ba0928f | |
|
|
6a5cc637ac | |
|
|
a2cf66476c | |
|
|
a254188f0c | |
|
|
3041d9f73c | |
|
|
908ddeb862 | |
|
|
3835e84450 | |
|
|
835d69d2d0 | |
|
|
4442daae53 | |
|
|
cfff2b808a | |
|
|
b8accefff7 | |
|
|
b8ef4d7937 | |
|
|
dcae12ab36 | |
|
|
ffa2e952c9 | |
|
|
e05c3b2471 | |
|
|
d59c5a20c1 | |
|
|
b1d449cf02 | |
|
|
3f06cc9aa6 | |
|
|
a5659fcb09 | |
|
|
6271a377bd | |
|
|
a4cee92d00 | |
|
|
f83f2a72ba | |
|
|
afb3e34e71 | |
|
|
ebd7811b12 | |
|
|
a64d671527 | |
|
|
7eaa5fce75 | |
|
|
da86ca6a2d | |
|
|
c4c1919df6 | |
|
|
869d982b1e | |
|
|
10839ba22c | |
|
|
398de3dc04 | |
|
|
4f432968ef | |
|
|
56cea50a65 | |
|
|
0d416c6f5a | |
|
|
c1d91839e4 | |
|
|
76189c6ad2 | |
|
|
b6f576d50d | |
|
|
a4ca1c86ec | |
|
|
38c9472cb1 | |
|
|
8a24448cb6 | |
|
|
a74bbfaee2 | |
|
|
ad0a20cc7e | |
|
|
51e578f2da | |
|
|
3563fa2b36 | |
|
|
f790decc4d | |
|
|
5a8f0201a3 | |
|
|
825355dd54 | |
|
|
c62535841a | |
|
|
b0d96ff67e | |
|
|
3607ccc92f | |
|
|
8570505758 | |
|
|
049a14e440 | |
|
|
1ec8275831 | |
|
|
9efe6adaf3 | |
|
|
a5b117e26d | |
|
|
01332ab77f | |
|
|
7aa3570b33 | |
|
|
e7866fda55 | |
|
|
cd9e0a7a10 | |
|
|
8223425b19 | |
|
|
9bf3a80341 | |
|
|
72a8016ab5 | |
|
|
9619a75427 | |
|
|
ceaac4e01b | |
|
|
aea755361b | |
|
|
e67c72951e | |
|
|
0fadf50bf4 | |
|
|
585ef604ff | |
|
|
99babe8bdf | |
|
|
bbd9b0204f | |
|
|
70eabe75dd | |
|
|
63de62111f | |
|
|
4cc9713fa7 | |
|
|
8710df7525 | |
|
|
9d23fd5b95 | |
|
|
4516b853cd | |
|
|
b3767cbbf0 | |
|
|
327e606deb | |
|
|
70e45d5ba2 | |
|
|
d4d3e15de3 | |
|
|
525c0e108f | |
|
|
73ad702684 | |
|
|
e67de4556c | |
|
|
0b0383003f | |
|
|
9ed98197f8 | |
|
|
fe5752cbff | |
|
|
c6b128270f | |
|
|
852d866073 | |
|
|
6a9ea8d4af | |
|
|
42c6179ba5 | |
|
|
5649a18633 | |
|
|
47f984cd28 | |
|
|
fc3a62e511 | |
|
|
5f0c890682 | |
|
|
59a069f51b | |
|
|
df796965f2 | |
|
|
26f9d2cf9f | |
|
|
a950093f8e | |
|
|
8842005898 | |
|
|
b4ae4bad41 | |
|
|
69e64397a0 | |
|
|
3d0795f04d | |
|
|
4abd0fb612 | |
|
|
dd3c9647e3 | |
|
|
8b2f4428bb | |
|
|
0dca416958 | |
|
|
1aa8c1ddfa | |
|
|
75d0ee2c1d | |
|
|
2e938dfc8d | |
|
|
1ffdd5d97e | |
|
|
276fbbec22 | |
|
|
05749ff122 | |
|
|
f131ad7148 | |
|
|
f50989b5ae | |
|
|
72adb9e713 | |
|
|
76e9ca3595 | |
|
|
b6f2ca355b | |
|
|
78370da460 | |
|
|
4d84a2d001 | |
|
|
0eb02698eb | |
|
|
c7b67b9042 | |
|
|
f53f837e3f | |
|
|
86644388f3 | |
|
|
c0e7185321 | |
|
|
9062f386de | |
|
|
3a2eaad493 | |
|
|
b70e868ac4 | |
|
|
bacb9f07c7 | |
|
|
2a28a36a69 | |
|
|
841ccc74ed | |
|
|
8729515537 | |
|
|
991e2a56b7 | |
|
|
c689198588 | |
|
|
667368d719 | |
|
|
6a0be0d780 | |
|
|
0573091162 | |
|
|
2ef0fc3bd9 | |
|
|
ce437aa86c | |
|
|
7ed299ead8 | |
|
|
f959a4ebb2 | |
|
|
7a5013524d | |
|
|
14a57fcae7 | |
|
|
47876b18bf | |
|
|
5abad0b1ba | |
|
|
1cdbdf2f09 | |
|
|
45a73793fb | |
|
|
581d9701bd | |
|
|
49ac86e6f9 | |
|
|
0f8ab1a75b | |
|
|
dda00ce228 | |
|
|
39a8255532 | |
|
|
b7eb4ce60b | |
|
|
d6a2bf21b1 | |
|
|
730f125d62 | |
|
|
8e065d3e51 | |
|
|
2a33457661 | |
|
|
0fbf0b04f4 | |
|
|
0c5d4f7158 | |
|
|
9e066372c5 | |
|
|
ca977625db | |
|
|
7d1cac6e2e | |
|
|
2229fa3545 | |
|
|
118eecbb59 | |
|
|
b8824dbbfb | |
|
|
3e751c0d8d | |
|
|
601ecf8887 | |
|
|
d13c617895 | |
|
|
e1f19b3919 | |
|
|
ba0162b3e1 | |
|
|
ed8804b7a7 | |
|
|
9be9eaaf80 | |
|
|
0683a18491 | |
|
|
6204888453 | |
|
|
c15637b347 | |
|
|
42c1f29a16 | |
|
|
8a193fa408 | |
|
|
4ee20adea7 | |
|
|
2df7d28d43 | |
|
|
43d1565dbe | |
|
|
f5dab20d67 | |
|
|
38cfb5f5e7 | |
|
|
b86c50a848 | |
|
|
694bc61dae | |
|
|
909efa720f | |
|
|
a8761bf679 | |
|
|
ad77530b6d | |
|
|
34d8da1562 | |
|
|
b73c7ee3cb | |
|
|
0d4f180a37 | |
|
|
829c44e347 | |
|
|
b15e93ac34 | |
|
|
f158e7728c | |
|
|
2b42899779 |
|
|
@ -0,0 +1,15 @@
|
|||
[*.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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/DeployDestroyLoop.mp3 (Stored with Git LFS)
BIN
Assets/DeployDestroyLoop.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/DeployDestroyStart.mp3 (Stored with Git LFS)
BIN
Assets/DeployDestroyStart.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/DurochkaLoop.mp3 (Stored with Git LFS)
BIN
Assets/DurochkaLoop.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/DurochkaStart.mp3 (Stored with Git LFS)
BIN
Assets/DurochkaStart.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/GorgorodLoop.mp3 (Stored with Git LFS)
BIN
Assets/GorgorodLoop.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/GorgorodStart.mp3 (Stored with Git LFS)
BIN
Assets/GorgorodStart.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/MoyaZhittyaLoop.mp3 (Stored with Git LFS)
BIN
Assets/MoyaZhittyaLoop.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/MoyaZhittyaStart.mp3 (Stored with Git LFS)
BIN
Assets/MoyaZhittyaStart.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/MuzikaGromcheLoop.mp3 (Stored with Git LFS)
BIN
Assets/MuzikaGromcheLoop.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/MuzikaGromcheStart.mp3 (Stored with Git LFS)
BIN
Assets/MuzikaGromcheStart.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Assets/VseVZaleLoop.mp3 (Stored with Git LFS)
BIN
Assets/VseVZaleLoop.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
Assets/VseVZaleStart.mp3 (Stored with Git LFS)
BIN
Assets/VseVZaleStart.mp3 (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,128 @@
|
|||
# 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.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<Project>
|
||||
<Target Name="NetcodePatch" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="dotnet netcode-patch -uv 2022.3.62 -nv 1.12.0 "$(TargetPath)" @(ReferencePathWithRefAssemblies->'"%(Identity)"', ' ')"/>
|
||||
</Target>
|
||||
</Project>
|
||||
8
Justfile
8
Justfile
|
|
@ -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/Oflor-MuzikaGromche/"
|
||||
plugin_dir := "$HOME/.config/r2modmanPlus-local/LethalCompany/profiles" / imperium_profile / "BepInEx/plugins/Ratijas-MuzikaGromche/"
|
||||
|
||||
install-imperium:
|
||||
rm -rf "{{ plugin_dir }}"
|
||||
|
|
@ -26,3 +26,9 @@ 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 }}"
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Solution>
|
||||
<Project Path="MuzikaGromche/MuzikaGromche.csproj" />
|
||||
</Solution>
|
||||
|
|
@ -2,9 +2,13 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using BepInEx;
|
||||
using BepInEx.Bootstrap;
|
||||
using LobbyCompatibility.Enums;
|
||||
using LobbyCompatibility.Features;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MuzikaGromche
|
||||
{
|
||||
internal static class Compatibility
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void Register(BaseUnityPlugin plugin)
|
||||
{
|
||||
if (Chainloader.PluginInfos.ContainsKey("BMX.LobbyCompatibility"))
|
||||
{
|
||||
RegisterLobbyCompatibility(plugin.Info.Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void RegisterLobbyCompatibility(BepInPlugin plugin)
|
||||
{
|
||||
PluginHelper.RegisterPlugin(plugin.GUID, plugin.Version, CompatibilityLevel.Everyone, VersionStrictness.Patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// ReSharper disable once CheckNamespace
|
||||
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,41 +2,79 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<AssemblyName>MuzikaGromche</AssemblyName>
|
||||
<Description>Opa che tut u nas</Description>
|
||||
<Version>13.37.6</Version>
|
||||
<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>
|
||||
<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"/>
|
||||
<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" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Assembly-CSharp" Publicize="true">
|
||||
<Reference Include="Assembly-CSharp" Publicize="true" Private="false">
|
||||
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Collections">
|
||||
<Reference Include="Unity.Collections" Private="false">
|
||||
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Netcode.Runtime" Publicize="true">
|
||||
<Reference Include="Unity.Netcode.Runtime" Publicize="true" Private="false">
|
||||
<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"/>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" Private="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="Bundle" AfterTargets="Build">
|
||||
|
|
@ -47,9 +85,12 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackagedResources Include="$(SolutionDir)README.md" />
|
||||
<PackagedResources Include="$(SolutionDir)CHANGELOG.md" />
|
||||
<PackagedResources Include="$(SolutionDir)icon.png" />
|
||||
<PackagedResources Include="$(SolutionDir)manifest.json" />
|
||||
<PackagedResources Include="$(TargetDir)MuzikaGromche.dll" />
|
||||
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
|
||||
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
|
||||
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -73,4 +114,25 @@
|
|||
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
|
|
@ -0,0 +1,346 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue