1
0
Fork 0

Compare commits

..

56 Commits
master ... dev

Author SHA1 Message Date
ivan tkachenko 6a9ea8d4af Release v1337.420.9001 2025-08-14 19:17:35 +03:00
ivan tkachenko 42c6179ba5 Add new track Beha with three variants of intro 2025-08-14 19:13:20 +03:00
ivan tkachenko 5649a18633 Split Track into Selectable and Audio interfaces, add support for groups 2025-08-14 18:48:54 +03:00
ivan tkachenko 47f984cd28 Allow tracks to share common audio clip files
Send one request per file name. File names can be explicitly overridden.
2025-08-14 15:38:59 +03:00
ivan tkachenko fc3a62e511 Rename Start segment to Intro to reduce some confusion
Confusingly, "start" may refer to too many things in different places,
while "intro" would unambiguously refer to an audio clip that plays
first before the loop starts.
2025-08-14 15:11:46 +03:00
ivan tkachenko 5f0c890682 Remove unused method 2025-08-14 15:09:31 +03:00
ivan tkachenko 59a069f51b Bump version 2025-08-14 15:09:27 +03:00
ivan tkachenko df796965f2 Release v1337.420.69 2025-08-11 22:28:57 +03:00
ivan tkachenko 26f9d2cf9f Print tracks length in debug builds, and remove unnecessary non-null assertion 2025-08-11 22:28:32 +03:00
ivan tkachenko a950093f8e Sort tracks by name, so they are easier to find in the config 2025-08-11 22:28:32 +03:00
ivan tkachenko 8842005898 Add new track BeefLiver 2025-08-11 22:28:31 +03:00
ivan tkachenko b4ae4bad41 Config: More usable range for fading out 2025-08-11 22:28:31 +03:00
ivan tkachenko 69e64397a0 Extrapolate AudioSource playback time to get smoother transitions
AudioSource only updates about 25 times per second, meaning that even at
30 fps some adjacent frames would be calculated as having exact same
timestamps and render duplicated colors. At 100+ fps more than 2/3 of
the frames would be duplicates.

As a drive-by change, split complex logic of BeatTimeState into smaller
classes. Most of the time the state needs to maintain some boolean flag
which it flips once and stays that way, like HasStarted, IsLooping.
2025-08-11 22:28:31 +03:00
ivan tkachenko 3d0795f04d Drop CSync as a dependency from Release builds
Since the rewrite of track selection to a custom netcode, CSync is only
needed for debug/development builds now.
2025-08-11 22:28:31 +03:00
ivan tkachenko 4abd0fb612 Fix stale event handlers causing errors in console 2025-08-11 22:28:30 +03:00
ivan tkachenko dd3c9647e3 Bump version 2025-08-11 22:28:29 +03:00
ivan tkachenko 8b2f4428bb Release v1337.69.420 2025-08-07 20:27:58 +03:00
ivan tkachenko 0dca416958 Rewrite track choosing event to custom netcode 2025-08-07 20:27:57 +03:00
ivan tkachenko 1aa8c1ddfa Fix Disco Ball hanging around after being disabled 2025-08-07 20:27:57 +03:00
ivan tkachenko 75d0ee2c1d Bump version 2025-08-07 20:27:57 +03:00
ivan tkachenko 2e938dfc8d Release v13.37.9001 2025-08-05 05:10:48 +03:00
ivan tkachenko 1ffdd5d97e Add spawn rate patch to make the event more likely 2025-08-05 05:10:21 +03:00
ivan tkachenko 276fbbec22 Clean up mention of removed config option "Enable Color Animations"
Amends 2a28a36a69
2025-08-05 05:10:11 +03:00
ivan tkachenko 05749ff122 Add Animator and Audio to MineshaftStartTile 2025-08-03 00:31:07 +03:00
ivan tkachenko f131ad7148 Fix NarrowHallwayTile2x2 mineshaft lights flickering 2025-08-03 00:31:07 +03:00
ivan tkachenko f50989b5ae Refactor: Optimize DiscoBallManager to create and cache at start of round 2025-08-03 00:31:06 +03:00
ivan tkachenko 72adb9e713 Refactor: Fix up visibility and static modifiers, and other minor things 2025-08-02 16:25:45 +03:00
ivan tkachenko 76e9ca3595 Refactor: Make State an internal class of JesterPatch class 2025-08-02 16:12:44 +03:00
ivan tkachenko b6f2ca355b Refactor: Factor out displaying lyrics as a tip in its own method 2025-08-02 15:54:07 +03:00
ivan tkachenko 78370da460 Fix LEDHangingLight (GarageTile & PoolTile) lights flickering 2025-08-02 15:50:59 +03:00
ivan tkachenko 4d84a2d001 Fix multiple Light components per animator
Add them all to the allPoweredLights list,
not just the whatever first one was found.
2025-08-02 15:50:59 +03:00
ivan tkachenko 0eb02698eb Fix KitchenTile lights flickering 2025-08-02 01:04:12 +03:00
ivan tkachenko c7b67b9042 Update manifest, README and project files 2025-08-02 01:04:11 +03:00
ivan tkachenko f53f837e3f Bundle CHANGELOG.md 2025-08-01 23:10:36 +03:00
ivan tkachenko 86644388f3 Bump version 2025-08-01 23:10:35 +03:00
ivan tkachenko c0e7185321 Release v13.37.1337 2025-08-01 16:49:42 +03:00
ivan tkachenko 9062f386de Fix/add light flickering with animator controllers 2025-08-01 16:48:16 +03:00
ivan tkachenko 3a2eaad493 Add more light flickering to the track Kach 2025-08-01 02:55:27 +03:00
ivan tkachenko b70e868ac4 Rename DiscoBall asset bundle
There is going to be another bundle, so we want some distinctive names.
2025-07-31 21:44:52 +03:00
ivan tkachenko bacb9f07c7 Use StartOfRound.Instance.audioListener for lyrics events
Probably doesn't make a difference, but it's nice to be able to
calculate audio source<->listener distance directly.
2025-07-30 20:09:17 +03:00
ivan tkachenko 2a28a36a69 Config: Remove EnableColorAnimations toggle
Turns out, it doesn't really affect anything. AMD on Linux would lag anyway.
2025-07-30 18:56:34 +03:00
ivan tkachenko 841ccc74ed Fix color transition from a negative beat 2025-07-30 18:56:33 +03:00
ivan tkachenko 8729515537 Fix timings of fade out and lyrics for DeployDestroy 2025-07-30 18:56:33 +03:00
ivan tkachenko 991e2a56b7 Fix color right before wrapping
The buggy Split method was erroneously creating a looping span despite
explicitly passing `isLooping: false` parameter because with
`beatToInclusive: LoopBeats` wrapping will occur regardless. This
messed up with Duration calculations, and eventually caused the last
beat default to transition with t=0, when it should really be static.
2025-07-30 18:56:33 +03:00
ivan tkachenko c689198588 Fix fading out: set pure black at the end 2025-07-30 18:56:33 +03:00
ivan tkachenko 667368d719 Add specialized color transition event to improve debug output 2025-07-30 18:37:59 +03:00
ivan tkachenko 6a0be0d780 Enable nullable reference types 2025-07-30 18:37:58 +03:00
ivan tkachenko 0573091162 Auto formatting 2025-07-30 18:37:58 +03:00
ivan tkachenko 2ef0fc3bd9 Fix up all logs to use nameof() instead of hardcoded string 2025-07-30 18:37:58 +03:00
ivan tkachenko ce437aa86c Events: Mark BaseEvent as abstract
It's not useful on its own
2025-07-30 18:37:57 +03:00
ivan tkachenko 7ed299ead8 Fix AudioSource distance check for lyrics event
It was checking maxDistance of a non-overridden loop clip during windup.
2025-07-30 18:37:56 +03:00
ivan tkachenko f959a4ebb2 Setup LobbyCompatibility as a dependency
This should help to avoid desync issues.
2025-07-30 01:29:07 +03:00
ivan tkachenko 7a5013524d Prevent Publicizer Warnings from Showing 2025-07-30 00:08:08 +03:00
ivan tkachenko 14a57fcae7 Mark referenced packages with Private attributes
Apparently, this is considered a good practice. Although Private="false"
is supposed to not copy the dependency into the output directory, which
didn't happen anyway?
2025-07-30 00:08:08 +03:00
ivan tkachenko 47876b18bf Fix up csproj XML formatting 2025-07-29 23:45:20 +03:00
ivan tkachenko 5abad0b1ba Bump version 2025-07-29 23:45:19 +03:00
36 changed files with 1524 additions and 404 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -1,5 +1,38 @@
# Changelog # Changelog
## 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 ## MuzikaGromche 13.37.911 - Sri Lanka Bus hotfix
- Fixed certain event sometimes not working due to wrong method call. - Fixed certain event sometimes not working due to wrong method call.

5
Directory.Build.targets Normal file
View File

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

View File

@ -11,7 +11,7 @@ build-debug:
clean: clean:
rm -rf dist MuzikaGromche/bin MuzikaGromche/obj 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: install-imperium:
rm -rf "{{ plugin_dir }}" rm -rf "{{ plugin_dir }}"

View File

@ -1,4 +1,6 @@
using DunGen; using DunGen;
using HarmonyLib;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -7,54 +9,21 @@ using UnityEngine;
namespace MuzikaGromche namespace MuzikaGromche
{ {
public class DiscoBallManager : MonoBehaviour public static class DiscoBallManager
{ {
// A struct holding a disco ball container object and the name of a tile for which it was designed. // A struct holding a disco ball container object and the name of a tile for which it was designed.
public readonly record struct Data(string TileName, GameObject DiscoBallContainer) private readonly record struct TilePatch(string TileName, GameObject DiscoBallContainer)
{ {
// We are specifically looking for cloned tiles, not the original prototypes. // We are specifically looking for cloned tiles, not the original prototypes.
public readonly string TileCloneName = $"{TileName}(Clone)"; public readonly string TileCloneName = $"{TileName}(Clone)";
} }
public static readonly List<Data> Containers = []; private static TilePatch[] Patches = [];
private static readonly List<GameObject> InstantiatedContainers = [];
public static void Initialize() private static readonly List<GameObject> CachedDiscoBalls = [];
{ private static readonly List<Animator> CachedDiscoBallAnimators = [];
string assetdir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "muzikagromche");
var bundle = AssetBundle.LoadFromFile(assetdir);
foreach ((string prefabPath, string tileName) in new[] { private static readonly string[] AnimatorContainersNames = [
("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"),
})
{
var container = bundle.LoadAsset<GameObject>(prefabPath);
Containers.Add(new(tileName, container));
}
}
public static void Enable()
{
// Just in case
Disable();
var query = from tile in Resources.FindObjectsOfTypeAll<Tile>()
join container in Containers
on tile.gameObject.name equals container.TileCloneName
select (tile, container);
foreach (var (tile, container) in query)
{
Enable(tile, container);
}
}
private static readonly string[] animatorNames = [
"DiscoBallProp/AnimContainer", "DiscoBallProp/AnimContainer",
"DiscoBallProp1/AnimContainer", "DiscoBallProp1/AnimContainer",
"DiscoBallProp2/AnimContainer", "DiscoBallProp2/AnimContainer",
@ -63,29 +32,134 @@ namespace MuzikaGromche
"DiscoBallProp5/AnimContainer", "DiscoBallProp5/AnimContainer",
]; ];
private static void Enable(Tile tile, Data container) public static void Load()
{ {
Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Enabling at '{tile.gameObject.name}'"); const string BundleFileName = "muzikagromche_discoball";
var discoBall = Instantiate(container.DiscoBallContainer, tile.transform); string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleFileName);
InstantiatedContainers.Add(discoBall); var assetBundle = AssetBundle.LoadFromFile(bundlePath)
?? throw new NullReferenceException("Failed to load bundle");
foreach (var animatorName in animatorNames) (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)
{ {
if (discoBall.transform.Find(animatorName)?.gameObject is GameObject animator) 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)
{ {
animator.GetComponent<Animator>().SetBool("on", true); 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);
Debug.Log($"{nameof(MuzikaGromche)} {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)
{
Debug.Log($"{nameof(MuzikaGromche)} {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() public static void Disable()
{ {
foreach (var discoBall in InstantiatedContainers) Toggle(false);
}
internal static void Clear()
{ {
Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)}: Disabling {discoBall.name}"); Debug.Log($"{nameof(MuzikaGromche)} {nameof(DiscoBallManager)} Clearing {CachedDiscoBalls.Count} disco balls & {CachedDiscoBallAnimators.Count} animators");
Destroy(discoBall); CachedDiscoBallAnimators.Clear();
CachedDiscoBalls.Clear();
} }
InstantiatedContainers.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

@ -2,41 +2,66 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>MuzikaGromche</AssemblyName> <Title>MuzikaGromche</Title>
<Description>Opa che tut u nas</Description> <PackageId>MuzikaGromche</PackageId>
<Version>13.37.911</Version> <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.420.9001</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<!-- NetcodePatch requires anything but 'full' -->
<DebugType>portable</DebugType>
<PackageReadmeFile>../README.md</PackageReadmeFile>
<PackageProjectUrl>https://git.vilunov.me/ratijas/muzika-gromche</PackageProjectUrl>
<RepositoryUrl>https://git.vilunov.me/ratijas/muzika-gromche</RepositoryUrl>
<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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all"/> <PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.Core" Version="5.*"/> <PackageReference Include="BepInEx.Core" Version="5.*" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*"/> <PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" PrivateAssets="all" Private="false" />
<PackageReference Include="UnityEngine.Modules" Version="2022.3.9" IncludeAssets="compile"/> <PackageReference Include="UnityEngine.Modules" Version="2022.3.9" PrivateAssets="all" Private="false" />
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" /> <PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" Private="false" />
<!-- <!--
Publicize internal methods, so we could generate config entries for tracks at runtime instead 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 of generating code at compile time. See https://github.com/lc-sigurd/CSync/issues/11
It is an optional dependency now, but there is no sane way to mark it as such.
--> -->
<PackageReference Include="Sigurd.BepInEx.CSync" Version="5.0.1" Publicize="true" /> <PackageReference Include="Sigurd.BepInEx.CSync" Version="5.0.1" Publicize="true" PrivateAssets="all" Private="false" />
<PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" /> <PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
</ItemGroup> </ItemGroup>
<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> <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference> </Reference>
<Reference Include="Unity.Collections"> <Reference Include="Unity.Collections" Private="false">
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath> <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath>
</Reference> </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> <HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Netcode.Runtime.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'"> <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> </ItemGroup>
<Target Name="Bundle" AfterTargets="Build"> <Target Name="Bundle" AfterTargets="Build">
@ -47,10 +72,12 @@
<ItemGroup> <ItemGroup>
<PackagedResources Include="$(SolutionDir)README.md" /> <PackagedResources Include="$(SolutionDir)README.md" />
<PackagedResources Include="$(SolutionDir)CHANGELOG.md" />
<PackagedResources Include="$(SolutionDir)icon.png" /> <PackagedResources Include="$(SolutionDir)icon.png" />
<PackagedResources Include="$(SolutionDir)manifest.json" /> <PackagedResources Include="$(SolutionDir)manifest.json" />
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche" /> <PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
<PackagedResources Include="$(TargetDir)MuzikaGromche.dll" /> <PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -83,7 +110,7 @@
--> -->
<Target Name="wav2ogg"> <Target Name="wav2ogg">
<ItemGroup> <ItemGroup>
<TrackNames Include="$(TrackName)Start" /> <TrackNames Include="$(TrackName)Intro" />
<TrackNames Include="$(TrackName)Loop" /> <TrackNames Include="$(TrackName)Loop" />
</ItemGroup> </ItemGroup>
<Exec Command="ffmpeg -bitexact -y -i $(WavExportDir)%(TrackNames.Identity).wav $(SolutionDir)Assets\%(TrackNames.Identity).ogg" /> <Exec Command="ffmpeg -bitexact -y -i $(WavExportDir)%(TrackNames.Identity).wav $(SolutionDir)Assets\%(TrackNames.Identity).ogg" />

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,290 @@
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
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;
Debug.Log($"{nameof(MuzikaGromche)} {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;
Debug.Log($"{nameof(MuzikaGromche)} {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);
// 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));
}
}
}
}

View File

@ -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);
Debug.Log($"{nameof(MuzikaGromche)} {nameof(SpawnRatePatch)} Overriding spawn weight[{index}] {weights[index]} * {multiplier} => {newWeight} for t={SpawnTime}");
weights[index] = newWeight;
}
}
}
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="BepInEx" value="https://nuget.bepinex.dev/v3/index.json" />
<add key="AAron Thunderstore" value="https://nuget.windows10ce.com/nuget/v3/index.json" />
</packageSources>
</configuration>

View File

@ -1,6 +1,7 @@
# Muzika Gromche! # Muzika Gromche!
Add some content to your reverse teleport experience on Titan! _Add some content to your Inverse teleporter experience on Titan!<sup>1</sup>_
This mod's name literally means "cranck music louder". This mod's name literally means "cranck music louder".
To keep it a surprise, it is adviced that you do not read the detailed description below. To keep it a surprise, it is adviced that you do not read the detailed description below.
@ -8,7 +9,7 @@ To keep it a surprise, it is adviced that you do not read the detailed descripti
Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes certain timers, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.* Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes certain timers, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.*
Muzika Gromche works with all Lethal Company versions from v72 all the way back to v40, and is likely to work on all future versions as long as dependencies ([`CSync`] and [`LethalConfig`]) are working. Muzika Gromche works with all Lethal Company versions from v72 all the way back to v40, and is likely to work on all future versions as long as dependencies ([`LethalConfig`] and [`LobbyCompatibility`]) are working.
Speaking of dependencies, [`V70PoweredLights_Fix`] is not strictly required, but it doesn't hurt to have it installed on any version, and it makes this mod more enjoyable on new Mansion tiles. Speaking of dependencies, [`V70PoweredLights_Fix`] is not strictly required, but it doesn't hurt to have it installed on any version, and it makes this mod more enjoyable on new Mansion tiles.
@ -16,10 +17,11 @@ Speaking of dependencies, [`V70PoweredLights_Fix`] is not strictly required, but
Configuration integrates with [`LethalConfig`] mod. Configuration integrates with [`LethalConfig`] mod.
If you are just trying out this mod for the first time, or want to experience it more often, consider toggling ON the "Override Spawn Rates" config entry. Otherwise it might take a frustratingly long time to find out what you need to find out.
Track selection options are only configurable by host player and only while orbiting. Track selection options are only configurable by host player and only while orbiting.
Any player can change their personal preferences locally. Any player can change their personal preferences locally.
- If you experience severe lags, try disabling color animations in config.
- If you are playing with a Bluetooth headset, adjust Audio Offset to -0.2 seconds. - If you are playing with a Bluetooth headset, adjust Audio Offset to -0.2 seconds.
- Display Lyrics toggle: show lyrics in a popup whenever player hears music. - Display Lyrics toggle: show lyrics in a popup whenever player hears music.
@ -30,7 +32,11 @@ Any player can change their personal preferences locally.
- [Just Nothing](https://t.me/REALJUSTNOTHING): Visual artist; contributed palettes, timings and animation curves. - [Just Nothing](https://t.me/REALJUSTNOTHING): Visual artist; contributed palettes, timings and animation curves.
- [WaterGun](https://www.youtube.com/channel/UCCxCFfmrnqkFZ8i9FsXBJVA): Created [`V70PoweredLights_Fix`] mod, patched certain tiles with amazing lightshow. - [WaterGun](https://www.youtube.com/channel/UCCxCFfmrnqkFZ8i9FsXBJVA): Created [`V70PoweredLights_Fix`] mod, patched certain tiles with amazing lightshow.
---
1. Actually not limited to Inverse teleporter or Titan.
[`CSync`]: https://thunderstore.io/c/lethal-company/p/Sigurd/CSync/ [`CSync`]: https://thunderstore.io/c/lethal-company/p/Sigurd/CSync/
[`LethalConfig`]: https://thunderstore.io/c/lethal-company/p/AinaVT/LethalConfig/ [`LethalConfig`]: https://thunderstore.io/c/lethal-company/p/AinaVT/LethalConfig/
[`LobbyCompatibility`]: https://thunderstore.io/c/lethal-company/p/BMX/LobbyCompatibility/
[`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/ [`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/

12
dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"evaisa.netcodepatcher.cli": {
"version": "4.3.0",
"commands": [
"netcode-patch"
]
}
}
}

View File

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