forked from nikita/muzika-gromche
Compare commits
5 Commits
c6b580af5a
...
f77e41bd17
| Author | SHA1 | Date |
|---|---|---|
|
|
f77e41bd17 | |
|
|
65784e726e | |
|
|
0d44728ef7 | |
|
|
d8651ce7db | |
|
|
f15e3a1e3d |
BIN
Assets/YesterdayIntro.ogg (Stored with Git LFS)
BIN
Assets/YesterdayIntro.ogg (Stored with Git LFS)
Binary file not shown.
BIN
Assets/YesterdayLoop.ogg (Stored with Git LFS)
BIN
Assets/YesterdayLoop.ogg (Stored with Git LFS)
Binary file not shown.
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,15 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## MuzikaGromche 1337.9001.69 - Six Seven Edition
|
## MuzikaGromche 1337.9001.69
|
||||||
|
|
||||||
- Added support for v80 (also known as "v81").
|
|
||||||
- Show real Artist & Song info in the config.
|
- Show real Artist & Song info in the config.
|
||||||
- Added a new track Yesterday.
|
- Integrated visual effects with QMK/VIA RGB keyboard (specifically input modules for Framework Laptop 16).
|
||||||
- Added lyrics to the existing track HighLow.
|
|
||||||
- New random track will be selected every time instead of having the same track all day.
|
|
||||||
- Groupped tracks (verses of the same song) are still played together in a sequence.
|
|
||||||
- Override Death Screen / Game Over text in more cases.
|
|
||||||
- Attempted to fix issue when Jester wouldn't select any track after the first one.
|
|
||||||
|
|
||||||
## MuzikaGromche 1337.9001.68 - LocalHost hotfix
|
## MuzikaGromche 1337.9001.68 - LocalHost hotfix
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<Project>
|
<Project>
|
||||||
<Target Name="NetcodePatch" AfterTargets="PostBuildEvent">
|
<Target Name="NetcodePatch" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="dotnet netcode-patch -uv 2022.3.62 -nv 1.12.2 "$(TargetPath)" @(ReferencePathWithRefAssemblies->'"%(Identity)"', ' ')"/>
|
<Exec Command="dotnet netcode-patch -uv 2022.3.62 -nv 1.12.0 "$(TargetPath)" @(ReferencePathWithRefAssemblies->'"%(Identity)"', ' ')"/>
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using GameNetcodeStuff;
|
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -22,12 +21,7 @@ namespace MuzikaGromche
|
||||||
SetTextImpl(text ?? GameOverTextModdedDefault);
|
SetTextImpl(text ?? GameOverTextModdedDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetTextAndClear(string? text)
|
public static IEnumerator SetTextAndClear(string? text)
|
||||||
{
|
|
||||||
HUDManager.Instance.StartCoroutine(SetTextAndClearImpl(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerator SetTextAndClearImpl(string? text)
|
|
||||||
{
|
{
|
||||||
SetText(text);
|
SetText(text);
|
||||||
// Game Over animation duration is about 4.25 seconds
|
// Game Over animation duration is about 4.25 seconds
|
||||||
|
|
@ -66,6 +60,7 @@ namespace MuzikaGromche
|
||||||
[HarmonyPatch(typeof(RoundManager))]
|
[HarmonyPatch(typeof(RoundManager))]
|
||||||
static class DeathScreenGameOverTextResetPatch
|
static class DeathScreenGameOverTextResetPatch
|
||||||
{
|
{
|
||||||
|
[HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
|
||||||
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
|
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
|
||||||
[HarmonyPrefix]
|
[HarmonyPrefix]
|
||||||
static void OnDestroy(RoundManager __instance)
|
static void OnDestroy(RoundManager __instance)
|
||||||
|
|
@ -74,29 +69,4 @@ namespace MuzikaGromche
|
||||||
DeathScreenGameOverTextManager.Clear();
|
DeathScreenGameOverTextManager.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HarmonyPatch(typeof(PlayerControllerB))]
|
|
||||||
static class PlayerControllerBKillPlayerPatch
|
|
||||||
{
|
|
||||||
// Use prefix to test distance from listener before the listener is reassigned
|
|
||||||
[HarmonyPatch(nameof(PlayerControllerB.KillPlayer))]
|
|
||||||
[HarmonyPrefix]
|
|
||||||
static void KillPlayerPrefix(PlayerControllerB __instance)
|
|
||||||
{
|
|
||||||
if (__instance.IsOwner && !__instance.isPlayerDead && __instance.AllowPlayerDeath())
|
|
||||||
{
|
|
||||||
// KILL LOCAL PLAYER
|
|
||||||
var list = Object.FindObjectsByType<EnemyAI>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
|
||||||
foreach (var jester in list)
|
|
||||||
{
|
|
||||||
if (jester.TryGetComponent<MuzikaGromcheJesterNetworkBehaviour>(out var behaviour) &&
|
|
||||||
behaviour.IsPlaying && Plugin.LocalPlayerCanHearMusic(jester))
|
|
||||||
{
|
|
||||||
behaviour.OverrideDeathScreenGameOverText();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,17 +113,11 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
foreach (var discoBall in CachedDiscoBalls)
|
foreach (var discoBall in CachedDiscoBalls)
|
||||||
{
|
{
|
||||||
if (discoBall != null)
|
discoBall.SetActive(on);
|
||||||
{
|
|
||||||
discoBall.SetActive(on);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
foreach (var animator in CachedDiscoBallAnimators)
|
foreach (var animator in CachedDiscoBallAnimators)
|
||||||
{
|
{
|
||||||
if (animator != null)
|
animator?.SetBool("on", on);
|
||||||
{
|
|
||||||
animator.SetBool("on", on);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -899,97 +899,6 @@ public static class Library
|
||||||
FadeOutDuration = 1.5f,
|
FadeOutDuration = 1.5f,
|
||||||
FlickerLightsTimeSeries = [-33, 39],
|
FlickerLightsTimeSeries = [-33, 39],
|
||||||
Lyrics = [
|
Lyrics = [
|
||||||
(-64, "Load up on guns"),
|
|
||||||
(-59, """
|
|
||||||
Load up on guns,
|
|
||||||
bring your friends
|
|
||||||
"""),
|
|
||||||
(-55, """
|
|
||||||
Load up on guns, It's
|
|
||||||
bring your friends fun
|
|
||||||
to lose
|
|
||||||
"""),
|
|
||||||
(-51, """
|
|
||||||
and
|
|
||||||
to
|
|
||||||
pretend
|
|
||||||
"""),
|
|
||||||
(-47, """
|
|
||||||
She's over-bored
|
|
||||||
"""),
|
|
||||||
(-43, """
|
|
||||||
She's over-bored
|
|
||||||
and self-assured
|
|
||||||
"""),
|
|
||||||
(-39, """
|
|
||||||
Oh no,
|
|
||||||
I know
|
|
||||||
"""),
|
|
||||||
(-35, """
|
|
||||||
Oh no,
|
|
||||||
I know
|
|
||||||
a dirty word
|
|
||||||
"""),
|
|
||||||
// 1
|
|
||||||
(-32, "Hello"),
|
|
||||||
(-30, """
|
|
||||||
|
|
||||||
HE LL O
|
|
||||||
"""),
|
|
||||||
(-28, """
|
|
||||||
H L
|
|
||||||
E L
|
|
||||||
O
|
|
||||||
"""),
|
|
||||||
(-26, """
|
|
||||||
W L O W
|
|
||||||
How W W W
|
|
||||||
W W
|
|
||||||
"""),
|
|
||||||
// 2
|
|
||||||
(-24, "Hello"),
|
|
||||||
(-22, """
|
|
||||||
|
|
||||||
L
|
|
||||||
H E L O
|
|
||||||
"""),
|
|
||||||
(-20, """
|
|
||||||
H LO
|
|
||||||
H E H L
|
|
||||||
H L OO
|
|
||||||
"""),
|
|
||||||
(-18, """
|
|
||||||
|
|
||||||
low
|
|
||||||
How
|
|
||||||
"""),
|
|
||||||
// 3
|
|
||||||
(-16, " Hello"),
|
|
||||||
(-14, """
|
|
||||||
L O
|
|
||||||
He LL
|
|
||||||
"""),
|
|
||||||
(-12, """
|
|
||||||
H H EEE L L OO
|
|
||||||
H<>h Ee L L O O
|
|
||||||
h h EEE LLL LLL OO
|
|
||||||
"""),
|
|
||||||
(-10, """
|
|
||||||
W w W
|
|
||||||
HOW w w W
|
|
||||||
W loW
|
|
||||||
"""),
|
|
||||||
// 4
|
|
||||||
( -8, "Hello"),
|
|
||||||
( -6, """
|
|
||||||
|
|
||||||
HE LL O
|
|
||||||
"""),
|
|
||||||
( -4, """
|
|
||||||
H L
|
|
||||||
L O
|
|
||||||
E
|
|
||||||
"""),
|
|
||||||
],
|
],
|
||||||
DrunknessLoopOffsetTimeSeries = new(
|
DrunknessLoopOffsetTimeSeries = new(
|
||||||
[-2f, -1f, 6f],
|
[-2f, -1f, 6f],
|
||||||
|
|
@ -1209,34 +1118,5 @@ public static class Library
|
||||||
[ 0f, 0.5f, 0f]),
|
[ 0f, 0.5f, 0f]),
|
||||||
GameOverText = "[LIFE SUPPORT: HEXTECH]",
|
GameOverText = "[LIFE SUPPORT: HEXTECH]",
|
||||||
},
|
},
|
||||||
new SelectableAudioTrack
|
|
||||||
{
|
|
||||||
Name = "Yesterday",
|
|
||||||
Artist = "BTS",
|
|
||||||
Song = "Not Today",
|
|
||||||
AudioType = AudioType.OGGVORBIS,
|
|
||||||
Language = Language.KOREAN,
|
|
||||||
WindUpTimer = 43.63f,
|
|
||||||
Bars = 16,
|
|
||||||
BeatsOffset = 0f,
|
|
||||||
ColorTransitionIn = 0.2f,
|
|
||||||
ColorTransitionOut = 0.3f,
|
|
||||||
ColorTransitionEasing = Easing.InOutExpo,
|
|
||||||
Palette = Palette.Parse([
|
|
||||||
"#F0FBFF", "#0B95FF", "#A8F5E2", "#B79BF2", "#fed2e1",
|
|
||||||
]),
|
|
||||||
LoopOffset = 0,
|
|
||||||
FadeOutBeat = -5f,
|
|
||||||
FadeOutDuration = 4f,
|
|
||||||
FlickerLightsTimeSeries = [-48.1f, -44.1f, -16, 15.9f, 31.9f],
|
|
||||||
Lyrics = [],
|
|
||||||
DrunknessLoopOffsetTimeSeries = new(
|
|
||||||
[-64, -63.5f, -58f, -34f, -31.75f, -22f, 0f, 0.25f, 6f],
|
|
||||||
[ 0f, 0.4f, 0f, 0f, 0.4f, 0f, 0f, 0.4f, 0f]),
|
|
||||||
CondensationLoopOffsetTimeSeries = new(
|
|
||||||
[32f, 32.25f, 38f],
|
|
||||||
[ 0f, 1f, 0f]),
|
|
||||||
GameOverText = "[LIFE SUPPORT: NOT TODAY]",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,13 @@
|
||||||
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" 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="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />
|
||||||
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
|
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
|
||||||
|
<PackageReference Include="HidSharp" Version="2.6.4" PrivateAssets="all" Private="false" GeneratePathProperty="true" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Assembly-CSharp" Publicize="true" Private="false">
|
<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="DunGen" Publicize="true" Private="false">
|
|
||||||
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\DunGen.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Unity.Collections" Private="false">
|
<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>
|
||||||
|
|
@ -92,7 +90,9 @@
|
||||||
<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_discoball" />
|
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
|
||||||
|
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
|
||||||
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
|
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
|
||||||
|
<PackagedResources Include="$(PkgHidSharp)\lib\netstandard2.0\*.dll" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,12 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
private static int GetCurrentSeed()
|
private static int GetCurrentSeed()
|
||||||
{
|
{
|
||||||
var rng = new System.Random(unchecked((int)DateTime.Now.Ticks));
|
var seed = 0;
|
||||||
var seed = rng.Next();
|
var roundManager = RoundManager.Instance;
|
||||||
|
if (roundManager != null && roundManager.dungeonGenerator != null)
|
||||||
|
{
|
||||||
|
seed = roundManager.dungeonGenerator.Generator.ChosenSeed;
|
||||||
|
}
|
||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,17 +158,19 @@ namespace MuzikaGromche
|
||||||
#endif
|
#endif
|
||||||
Config = new Config(base.Config);
|
Config = new Config(base.Config);
|
||||||
DiscoBallManager.Load();
|
DiscoBallManager.Load();
|
||||||
|
PoweredLightsAnimators.Load();
|
||||||
Harmony = new Harmony(MyPluginInfo.PLUGIN_NAME);
|
Harmony = new Harmony(MyPluginInfo.PLUGIN_NAME);
|
||||||
Harmony.PatchAll(typeof(GameNetworkManagerPatch));
|
Harmony.PatchAll(typeof(GameNetworkManagerPatch));
|
||||||
Harmony.PatchAll(typeof(JesterPatch));
|
Harmony.PatchAll(typeof(JesterPatch));
|
||||||
|
Harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch));
|
||||||
Harmony.PatchAll(typeof(AllPoweredLightsPatch));
|
Harmony.PatchAll(typeof(AllPoweredLightsPatch));
|
||||||
Harmony.PatchAll(typeof(DiscoBallTilePatch));
|
Harmony.PatchAll(typeof(DiscoBallTilePatch));
|
||||||
Harmony.PatchAll(typeof(DiscoBallDespawnPatch));
|
Harmony.PatchAll(typeof(DiscoBallDespawnPatch));
|
||||||
Harmony.PatchAll(typeof(SpawnRatePatch));
|
Harmony.PatchAll(typeof(SpawnRatePatch));
|
||||||
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
||||||
Harmony.PatchAll(typeof(PlayerControllerBKillPlayerPatch));
|
|
||||||
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
||||||
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
||||||
|
Harmony.PatchAll(typeof(Via.ViaFlickerLightsPatch));
|
||||||
NetcodePatcher();
|
NetcodePatcher();
|
||||||
Compatibility.Register(this);
|
Compatibility.Register(this);
|
||||||
}
|
}
|
||||||
|
|
@ -342,8 +348,6 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
internal IAudioTrack[] GetTracks();
|
internal IAudioTrack[] GetTracks();
|
||||||
|
|
||||||
internal int Count() => GetTracks().Length;
|
|
||||||
|
|
||||||
// Index is a non-negative monotonically increasing number of times
|
// Index is a non-negative monotonically increasing number of times
|
||||||
// this ISelectableTrack has been played for this Jester on this day.
|
// this ISelectableTrack has been played for this Jester on this day.
|
||||||
// A group of tracks can use this index to rotate tracks sequentially.
|
// A group of tracks can use this index to rotate tracks sequentially.
|
||||||
|
|
@ -587,8 +591,6 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
|
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
|
||||||
|
|
||||||
int ISelectableTrack.Count() => 1;
|
|
||||||
|
|
||||||
IAudioTrack ISelectableTrack.SelectTrack(int index) => this;
|
IAudioTrack ISelectableTrack.SelectTrack(int index) => this;
|
||||||
|
|
||||||
void ISelectableTrack.Debug()
|
void ISelectableTrack.Debug()
|
||||||
|
|
@ -613,8 +615,6 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
|
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
|
||||||
|
|
||||||
int ISelectableTrack.Count() => Tracks.Length;
|
|
||||||
|
|
||||||
IAudioTrack ISelectableTrack.SelectTrack(int index)
|
IAudioTrack ISelectableTrack.SelectTrack(int index)
|
||||||
{
|
{
|
||||||
if (Tracks.Length == 0)
|
if (Tracks.Length == 0)
|
||||||
|
|
@ -1329,6 +1329,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
// Calculate final color, substituting null with initialColor if needed.
|
// Calculate final color, substituting null with initialColor if needed.
|
||||||
public abstract Color GetColor(Color initialColor);
|
public abstract Color GetColor(Color initialColor);
|
||||||
|
public abstract Color? GetNullableColor();
|
||||||
|
|
||||||
protected string NullableColorToString(Color? color)
|
protected string NullableColorToString(Color? color)
|
||||||
{
|
{
|
||||||
|
|
@ -1345,6 +1346,11 @@ namespace MuzikaGromche
|
||||||
return Color ?? initialColor;
|
return Color ?? initialColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Color? GetNullableColor()
|
||||||
|
{
|
||||||
|
return Color;
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"Color(#{NullableColorToString(Color)})";
|
return $"Color(#{NullableColorToString(Color)})";
|
||||||
|
|
@ -1366,7 +1372,7 @@ namespace MuzikaGromche
|
||||||
return Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f));
|
return Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color? GetNullableColor()
|
public override Color? GetNullableColor()
|
||||||
{
|
{
|
||||||
return From is { } from && To is { } to ? Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f)) : null;
|
return From is { } from && To is { } to ? Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f)) : null;
|
||||||
}
|
}
|
||||||
|
|
@ -2046,6 +2052,11 @@ namespace MuzikaGromche
|
||||||
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
|
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
|
||||||
const string LoopAudioGameObjectName = "MuzikaGromcheAudio (Loop)";
|
const string LoopAudioGameObjectName = "MuzikaGromcheAudio (Loop)";
|
||||||
|
|
||||||
|
// Number of times a selected track has been played.
|
||||||
|
// Increases by 1 with each ChooseTrackServerRpc call.
|
||||||
|
// Resets on SettingChanged.
|
||||||
|
private int SelectedTrackIndex = 0;
|
||||||
|
|
||||||
internal IAudioTrack? CurrentTrack = null;
|
internal IAudioTrack? CurrentTrack = null;
|
||||||
internal BeatTimeState? BeatTimeState = null;
|
internal BeatTimeState? BeatTimeState = null;
|
||||||
internal AudioSource IntroAudioSource = null!;
|
internal AudioSource IntroAudioSource = null!;
|
||||||
|
|
@ -2090,6 +2101,8 @@ namespace MuzikaGromche
|
||||||
public override void OnDestroy()
|
public override void OnDestroy()
|
||||||
{
|
{
|
||||||
Config.Volume.SettingChanged -= UpdateVolume;
|
Config.Volume.SettingChanged -= UpdateVolume;
|
||||||
|
|
||||||
|
DeathScreenGameOverTextManager.Clear();
|
||||||
Stop();
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2104,7 +2117,6 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
HostIsModded = IsServer;
|
|
||||||
ChooseTrackDeferred();
|
ChooseTrackDeferred();
|
||||||
foreach (var track in Plugin.Tracks)
|
foreach (var track in Plugin.Tracks)
|
||||||
{
|
{
|
||||||
|
|
@ -2135,8 +2147,7 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
|
private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
HostSelectableTrack = null;
|
SelectedTrackIndex = 0;
|
||||||
HostSelectableTrackIndex = 0;
|
|
||||||
ChooseTrackDeferred();
|
ChooseTrackDeferred();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2157,8 +2168,7 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once host has set a track via RPC, it is considered modded, and expected to always set tracks, so never reset this flag back to false.
|
// Once host has set a track via RPC, it is considered modded, and expected to always set tracks, so never reset this flag back to false.
|
||||||
// Initialized to `IsServer` on network spawn. If I am the host, then I am modded, otherwise we'll find out later.
|
bool HostIsModded = false;
|
||||||
bool HostIsModded;
|
|
||||||
|
|
||||||
// Playing with modded host automatically disables vanilla compatability mode
|
// Playing with modded host automatically disables vanilla compatability mode
|
||||||
public bool VanillaCompatMode => IsServer ? Config.VanillaCompatMode : !HostIsModded;
|
public bool VanillaCompatMode => IsServer ? Config.VanillaCompatMode : !HostIsModded;
|
||||||
|
|
@ -2172,12 +2182,12 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
if (Config.VanillaCompatMode)
|
if (Config.VanillaCompatMode)
|
||||||
{
|
{
|
||||||
// In vanilla compat mode, no matter whether you are a host or a client, you should skip networking anyway
|
// In vanilla compat mode no, matter whether you are a host or a client, you should skip networking anyway
|
||||||
ChooseTrackCompat();
|
ChooseTrackCompat();
|
||||||
}
|
}
|
||||||
else if (IsServer)
|
else if (IsServer)
|
||||||
{
|
{
|
||||||
ChooseTrackOnServer();
|
ChooseTrackServerRpc();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -2194,7 +2204,7 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Rpc(SendTo.Everyone)]
|
[ClientRpc]
|
||||||
void SetTrackClientRpc(string name)
|
void SetTrackClientRpc(string name)
|
||||||
{
|
{
|
||||||
Plugin.Log.LogDebug($"SetTrackClientRpc {name}");
|
Plugin.Log.LogDebug($"SetTrackClientRpc {name}");
|
||||||
|
|
@ -2217,25 +2227,14 @@ namespace MuzikaGromche
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host selected this group of tracks, and
|
[ServerRpc]
|
||||||
private ISelectableTrack? HostSelectableTrack = null;
|
void ChooseTrackServerRpc()
|
||||||
|
|
||||||
// Number of times a selected track has been played.
|
|
||||||
// Increases by 1 with each ChooseTrackOnServer call.
|
|
||||||
// Resets on SettingChanged.
|
|
||||||
private int HostSelectableTrackIndex = 0;
|
|
||||||
|
|
||||||
void ChooseTrackOnServer()
|
|
||||||
{
|
{
|
||||||
if (HostSelectableTrack == null || HostSelectableTrackIndex >= HostSelectableTrack.Count())
|
var selectableTrack = Plugin.ChooseTrack();
|
||||||
{
|
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
|
||||||
HostSelectableTrack = Plugin.ChooseTrack();
|
Plugin.Log.LogInfo($"ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}");
|
||||||
HostSelectableTrackIndex = 0;
|
|
||||||
}
|
|
||||||
var audioTrack = HostSelectableTrack.SelectTrack(HostSelectableTrackIndex);
|
|
||||||
Plugin.Log.LogInfo($"ChooseTrackOnServer {HostSelectableTrack.Name} #{HostSelectableTrackIndex} {audioTrack.Name}");
|
|
||||||
SetTrackClientRpc(audioTrack.Name);
|
SetTrackClientRpc(audioTrack.Name);
|
||||||
HostSelectableTrackIndex += 1;
|
SelectedTrackIndex += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChooseTrackCompat()
|
void ChooseTrackCompat()
|
||||||
|
|
@ -2348,6 +2347,7 @@ namespace MuzikaGromche
|
||||||
internal void Stop()
|
internal void Stop()
|
||||||
{
|
{
|
||||||
PoweredLightsBehaviour.Instance.ResetLightColor();
|
PoweredLightsBehaviour.Instance.ResetLightColor();
|
||||||
|
Via.ViaBehaviour.Instance.Restore();
|
||||||
DiscoBallManager.Disable();
|
DiscoBallManager.Disable();
|
||||||
ScreenFiltersManager.Clear();
|
ScreenFiltersManager.Clear();
|
||||||
|
|
||||||
|
|
@ -2377,9 +2377,7 @@ namespace MuzikaGromche
|
||||||
// Just in case if players have spawned multiple Jesters,
|
// Just in case if players have spawned multiple Jesters,
|
||||||
// Don't reset Config.CurrentTrack to null,
|
// Don't reset Config.CurrentTrack to null,
|
||||||
// so that the latest chosen track remains set.
|
// so that the latest chosen track remains set.
|
||||||
// Don't reset MuzikaGromcheJesterNetworkBehaviour.CurrentTrack to null,
|
CurrentTrack = null;
|
||||||
// because it may have already been set by host via RPC.
|
|
||||||
// CurrentTrack = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OverrideDeathScreenGameOverText()
|
public void OverrideDeathScreenGameOverText()
|
||||||
|
|
@ -2389,7 +2387,7 @@ namespace MuzikaGromche
|
||||||
// Playing as a client with a host who doesn't have the mod
|
// Playing as a client with a host who doesn't have the mod
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DeathScreenGameOverTextManager.SetTextAndClear(CurrentTrack.GameOverText);
|
StartCoroutine(DeathScreenGameOverTextManager.SetTextAndClear(CurrentTrack.GameOverText));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2559,9 +2557,18 @@ namespace MuzikaGromche
|
||||||
|
|
||||||
case SetLightsColorEvent e:
|
case SetLightsColorEvent e:
|
||||||
PoweredLightsBehaviour.Instance.SetLightColor(e);
|
PoweredLightsBehaviour.Instance.SetLightColor(e);
|
||||||
|
if (localPlayerCanHearMusic && e.GetNullableColor() is { } color)
|
||||||
|
{
|
||||||
|
Via.ViaBehaviour.Instance.SetColor(color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Via.ViaBehaviour.Instance.Restore();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FlickerLightsEvent:
|
case FlickerLightsEvent:
|
||||||
|
// VIA is handled by a Harmony patch to integrate with all flickering events, not just from this mod.
|
||||||
RoundManager.Instance.FlickerLights(true);
|
RoundManager.Instance.FlickerLights(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,245 @@
|
||||||
|
using DunGen;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
using MuzikaGromche.Via;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace MuzikaGromche
|
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))]
|
[HarmonyPatch(typeof(RoundManager))]
|
||||||
static class AllPoweredLightsPatch
|
static class AllPoweredLightsPatch
|
||||||
{
|
{
|
||||||
|
|
@ -14,11 +250,47 @@ namespace MuzikaGromche
|
||||||
// - (maybe more?)
|
// - (maybe more?)
|
||||||
// In order to fix that, replace singular GetComponentInChildren<Light> with plural GetComponentsInChildren<Light> version.
|
// In order to fix that, replace singular GetComponentInChildren<Light> with plural GetComponentsInChildren<Light> version.
|
||||||
[HarmonyPatch(nameof(RoundManager.RefreshLightsList))]
|
[HarmonyPatch(nameof(RoundManager.RefreshLightsList))]
|
||||||
[HarmonyPostfix]
|
[HarmonyPrefix]
|
||||||
static void OnRefreshLightsList(RoundManager __instance)
|
static bool OnRefreshLightsList(RoundManager __instance)
|
||||||
{
|
{
|
||||||
|
RefreshLightsListPatched(__instance);
|
||||||
|
|
||||||
var behaviour = __instance.gameObject.GetComponent<PoweredLightsBehaviour>() ?? __instance.gameObject.AddComponent<PoweredLightsBehaviour>();
|
var behaviour = __instance.gameObject.GetComponent<PoweredLightsBehaviour>() ?? __instance.gameObject.AddComponent<PoweredLightsBehaviour>();
|
||||||
behaviour.Refresh();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,11 +340,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
foreach (var data in AllPoweredLights)
|
foreach (var data in AllPoweredLights)
|
||||||
{
|
{
|
||||||
var light = data.Light;
|
data.Light.color = data.InitialColor;
|
||||||
if (light != null)
|
|
||||||
{
|
|
||||||
light.color = data.InitialColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
drunkness = 0f;
|
drunkness = 0f;
|
||||||
// Only the stop animation if vanilla doesn't animate TZP right now.
|
// Only the stop animation if vanilla doesn't animate TZP right now.
|
||||||
// Objects may be null on shutdown
|
if (GameNetworkManager.Instance.localPlayerController.drunkness == 0f)
|
||||||
var network = GameNetworkManager.Instance;
|
|
||||||
if (network != null && network.localPlayerController != null && network.localPlayerController.drunkness == 0f)
|
|
||||||
{
|
{
|
||||||
HUDManager.Instance.gasHelmetAnimator.SetBool("gasEmitting", value: false);
|
HUDManager.Instance.gasHelmetAnimator.SetBool("gasEmitting", value: false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
if (EnemyIndex is int index)
|
if (EnemyIndex is int index)
|
||||||
{
|
{
|
||||||
if (__instance.InsideEnemyCannotBeSpawned(index, __instance.currentEnemyPower))
|
if (__instance.EnemyCannotBeSpawned(index))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MuzikaGromche.Via;
|
||||||
|
|
||||||
|
public sealed class AsyncEventProcessor<T> where T : struct
|
||||||
|
{
|
||||||
|
private readonly object stateLock = new();
|
||||||
|
|
||||||
|
private T latestData;
|
||||||
|
private T lastProcessedData;
|
||||||
|
private bool isShutdownRequested;
|
||||||
|
private TaskCompletionSource<bool> signal = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
public delegate ValueTask ProcessEventAsync(T oldData, T newData, bool shutdown);
|
||||||
|
private readonly ProcessEventAsync processEventAsync;
|
||||||
|
|
||||||
|
public AsyncEventProcessor(T initialData, ProcessEventAsync processEventAsync)
|
||||||
|
{
|
||||||
|
latestData = initialData;
|
||||||
|
lastProcessedData = initialData;
|
||||||
|
this.processEventAsync = processEventAsync;
|
||||||
|
_ = Task.Run(ProcessLoopAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signals the processor that new data is available.
|
||||||
|
/// If <c>requestShutdown</c> is set, the processor will perform one last pass before exiting.
|
||||||
|
/// </summary>
|
||||||
|
public void Notify(T data, bool requestShutdown = false)
|
||||||
|
{
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
latestData = data;
|
||||||
|
if (requestShutdown)
|
||||||
|
{
|
||||||
|
isShutdownRequested = true;
|
||||||
|
}
|
||||||
|
// Trigger the task to wake up
|
||||||
|
signal.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessLoopAsync()
|
||||||
|
{
|
||||||
|
bool running = true;
|
||||||
|
|
||||||
|
while (running)
|
||||||
|
{
|
||||||
|
Task<bool> nextSignal;
|
||||||
|
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
nextSignal = signal.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a notification or shutdown signal
|
||||||
|
//
|
||||||
|
// VSTHRD003 fix: We are awaiting a task we didn't "start",
|
||||||
|
// but by using RunContinuationsAsynchronously in the TCS constructor,
|
||||||
|
// we guarantee the 'await' won't hijack the signaler's thread.
|
||||||
|
await nextSignal.ConfigureAwait(false);
|
||||||
|
|
||||||
|
T newData;
|
||||||
|
T oldData;
|
||||||
|
|
||||||
|
// Reset the signal for the next round
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
signal = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
if (isShutdownRequested)
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
newData = latestData;
|
||||||
|
oldData = lastProcessedData;
|
||||||
|
lastProcessedData = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
await processEventAsync(oldData, newData, !running);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using HidSharp;
|
||||||
|
|
||||||
|
namespace MuzikaGromche.Via;
|
||||||
|
|
||||||
|
class DevicePool<T> : IDisposable
|
||||||
|
where T : IDevicePoolDelegate
|
||||||
|
{
|
||||||
|
private readonly IDevicePoolFactory<T> sidecarFactory;
|
||||||
|
// Async synchronization
|
||||||
|
private readonly SemaphoreSlim semaphore;
|
||||||
|
// Async access, use semaphore!
|
||||||
|
private readonly Dictionary<string, (HidDevice, T)> existing = [];
|
||||||
|
|
||||||
|
private bool updating = false;
|
||||||
|
|
||||||
|
public DevicePool(IDevicePoolFactory<T> sidecarFactory)
|
||||||
|
{
|
||||||
|
semaphore = new SemaphoreSlim(1, 1);
|
||||||
|
this.sidecarFactory = sidecarFactory;
|
||||||
|
DeviceList.Local.Changed += (_sender, _args) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Pool Changed");
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(300);
|
||||||
|
if (!updating)
|
||||||
|
{
|
||||||
|
updating = true;
|
||||||
|
UpdateConnections();
|
||||||
|
updating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
UpdateConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithSemaphore(Action action)
|
||||||
|
{
|
||||||
|
semaphore.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action.Invoke();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Pool Dispose");
|
||||||
|
Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Clear()
|
||||||
|
{
|
||||||
|
WithSemaphore(ClearUnsafe);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearUnsafe()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Pool Clear");
|
||||||
|
ForEachUnsafe((sidecar) => sidecar.Dispose());
|
||||||
|
existing.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearUnsafe(string path)
|
||||||
|
{
|
||||||
|
if (existing.Remove(path, out var data))
|
||||||
|
{
|
||||||
|
var (_, sidecar) = data;
|
||||||
|
sidecar.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Restore()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Pool Restore");
|
||||||
|
ForEach((sidecar) => sidecar.Restore());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateConnections()
|
||||||
|
{
|
||||||
|
WithSemaphore(() =>
|
||||||
|
{
|
||||||
|
// set of removed devices to be cleaned up
|
||||||
|
var removed = existing.Keys.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var hidDevice in DeviceList.Local.GetHidDevices())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// surely path is enough to uniquely differentiate
|
||||||
|
var path = hidDevice.DevicePath;
|
||||||
|
if (existing.ContainsKey(path))
|
||||||
|
{
|
||||||
|
// not gone anywhere
|
||||||
|
removed.Remove(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sidecar = sidecarFactory.Create(hidDevice);
|
||||||
|
if (sidecar != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Pool Added {path}");
|
||||||
|
existing[path] = (hidDevice, sidecar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var path in removed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ClearUnsafe(path);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ForEach(Action<T> action)
|
||||||
|
{
|
||||||
|
WithSemaphore(() => ForEachUnsafe(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForEachUnsafe(Action<T> action)
|
||||||
|
{
|
||||||
|
foreach (var (_, sidecar) in existing.Values)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action(sidecar);
|
||||||
|
}
|
||||||
|
// ignore. the faulty device will be removed soon.
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispose should call Restore
|
||||||
|
interface IDevicePoolDelegate : IDisposable
|
||||||
|
{
|
||||||
|
void Restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDevicePoolFactory<T>
|
||||||
|
{
|
||||||
|
T? Create(HidDevice hidDevice);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using HidSharp;
|
||||||
|
|
||||||
|
namespace MuzikaGromche.Via;
|
||||||
|
|
||||||
|
class FrameworkDeviceFactory : IDevicePoolFactory<ViaDeviceDelegate>
|
||||||
|
{
|
||||||
|
// Framework Vendor ID
|
||||||
|
private const int VID = 0x32AC;
|
||||||
|
internal static readonly int[] PIDs =
|
||||||
|
[
|
||||||
|
0x0012, // Framework Laptop 16 Keyboard Module - ANSI
|
||||||
|
0x0013, // Framework Laptop 16 RGB macropad module
|
||||||
|
];
|
||||||
|
// VIA/QMK Raw HID identifiers
|
||||||
|
private const ushort UsagePage = 0xFF60;
|
||||||
|
private const byte Usage = 0x61;
|
||||||
|
|
||||||
|
// In case of Framework Laptop 16 RGB Keyboard and Macropad,
|
||||||
|
// Usage Page & Usage are encoded at the start of the report descriptor.
|
||||||
|
private static readonly byte[] UsagePageAndUsageEncoded = [0x06, 0x60, 0xFF, 0x09, 0x61];
|
||||||
|
|
||||||
|
private const byte ExpectedVersion = 12;
|
||||||
|
|
||||||
|
private static bool DevicePredicate(HidDevice device)
|
||||||
|
{
|
||||||
|
if (VID != device.VendorID)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!PIDs.Contains(device.ProductID))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var report = device.GetRawReportDescriptor();
|
||||||
|
if (report == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!report.AsSpan().StartsWith(UsagePageAndUsageEncoded))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ViaDeviceDelegate? Create(HidDevice hidDevice)
|
||||||
|
{
|
||||||
|
if (!DevicePredicate(hidDevice))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!hidDevice.TryOpen(out var stream))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var via = new ViaKeyboardApi(stream);
|
||||||
|
var version = via.GetProtocolVersion();
|
||||||
|
if (version != ExpectedVersion)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var brightness = via.GetRgbMatrixBrightness();
|
||||||
|
if (brightness == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var effect = via.GetRgbMatrixEffect();
|
||||||
|
if (effect == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var color = via.GetRgbMatrixColor();
|
||||||
|
if (color == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ViaDeviceDelegate(hidDevice, stream, via, brightness.Value, effect.Value, color.Value.Hue, color.Value.Saturation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViaDeviceDelegate : IDevicePoolDelegate, ILightshow, IDisposable
|
||||||
|
{
|
||||||
|
// Objects
|
||||||
|
public readonly HidDevice device;
|
||||||
|
public readonly HidStream stream;
|
||||||
|
public readonly ViaKeyboardApi via;
|
||||||
|
|
||||||
|
private readonly AsyncEventProcessor<ViaDeviceState> asyncEventProcessor;
|
||||||
|
private readonly ViaDeviceState initialState;
|
||||||
|
private byte? brightnessOverride = null;
|
||||||
|
private const ViaEffectMode EFFECT = ViaEffectMode.Static;
|
||||||
|
private byte? effectOverride = null;
|
||||||
|
private ColorHSV? color = null;
|
||||||
|
|
||||||
|
public ViaDeviceDelegate(HidDevice device, HidStream stream, ViaKeyboardApi via, byte brightness, byte effect, byte hue, byte saturation)
|
||||||
|
{
|
||||||
|
// Objects
|
||||||
|
this.device = device;
|
||||||
|
this.stream = stream;
|
||||||
|
this.via = via;
|
||||||
|
// Initial values
|
||||||
|
initialState = new()
|
||||||
|
{
|
||||||
|
Brightness = brightness,
|
||||||
|
Effect = effect,
|
||||||
|
Hue = hue,
|
||||||
|
Saturation = saturation,
|
||||||
|
};
|
||||||
|
asyncEventProcessor = new AsyncEventProcessor<ViaDeviceState>(initialState, ProcessDataAsync);
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Restore()
|
||||||
|
{
|
||||||
|
brightnessOverride = null;
|
||||||
|
effectOverride = null;
|
||||||
|
color = null;
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Simplified version of Restore()
|
||||||
|
brightnessOverride = null;
|
||||||
|
effectOverride = null;
|
||||||
|
color = null;
|
||||||
|
asyncEventProcessor.Notify(initialState, requestShutdown: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetBrightnessOverride(byte? brightness)
|
||||||
|
{
|
||||||
|
brightnessOverride = brightness;
|
||||||
|
effectOverride = (byte)EFFECT;
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetColor(byte hue, byte saturation, byte value)
|
||||||
|
{
|
||||||
|
color = new ColorHSV(hue, saturation, value);
|
||||||
|
effectOverride = (byte)EFFECT;
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Notify()
|
||||||
|
{
|
||||||
|
asyncEventProcessor.Notify(new()
|
||||||
|
{
|
||||||
|
Brightness = brightnessOverride ?? color?.Value ?? initialState.Brightness,
|
||||||
|
Effect = effectOverride ?? initialState.Effect,
|
||||||
|
Hue = color?.Hue ?? initialState.Hue,
|
||||||
|
Saturation = color?.Saturation ?? initialState.Saturation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask ProcessDataAsync(ViaDeviceState oldData, ViaDeviceState newData, bool shutdown)
|
||||||
|
{
|
||||||
|
var cancellationToken = CancellationToken.None;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (oldData.Brightness != newData.Brightness)
|
||||||
|
{
|
||||||
|
await via.SetRgbMatrixBrightnessAsync(newData.Brightness, cancellationToken);
|
||||||
|
}
|
||||||
|
if (oldData.Effect != newData.Effect)
|
||||||
|
{
|
||||||
|
await via.SetRgbMatrixEffectAsync(newData.Effect, cancellationToken);
|
||||||
|
}
|
||||||
|
if (oldData.Hue != newData.Hue || oldData.Saturation != newData.Saturation)
|
||||||
|
{
|
||||||
|
await via.SetRgbMatrixColorAsync(newData.Hue, newData.Saturation, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (shutdown)
|
||||||
|
{
|
||||||
|
stream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DevicePoolLightshow<T> : ILightshow
|
||||||
|
where T: IDevicePoolDelegate, ILightshow
|
||||||
|
{
|
||||||
|
private readonly DevicePool<T> devicePool;
|
||||||
|
|
||||||
|
public DevicePoolLightshow(DevicePool<T> devicePool)
|
||||||
|
{
|
||||||
|
this.devicePool = devicePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetBrightnessOverride(byte? brightness)
|
||||||
|
{
|
||||||
|
devicePool.ForEach(d =>
|
||||||
|
{
|
||||||
|
d.SetBrightnessOverride(brightness);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetColor(byte hue, byte saturation, byte value)
|
||||||
|
{
|
||||||
|
devicePool.ForEach(d =>
|
||||||
|
{
|
||||||
|
d.SetColor(hue, saturation, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ViaDeviceState
|
||||||
|
{
|
||||||
|
public byte Brightness;
|
||||||
|
public byte Effect;
|
||||||
|
public byte Hue;
|
||||||
|
public byte Saturation;
|
||||||
|
}
|
||||||
|
|
||||||
|
record struct ColorHSV(byte Hue, byte Saturation, byte Value);
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MuzikaGromche.Via;
|
||||||
|
|
||||||
|
interface ILightshow
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Override or reset brightness (value) for flickering animation.
|
||||||
|
/// </summary>
|
||||||
|
void SetBrightnessOverride(byte? brightness);
|
||||||
|
void SetColor(byte hue, byte saturation, byte value);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Animations
|
||||||
|
{
|
||||||
|
private static CancellationTokenSource? cts;
|
||||||
|
|
||||||
|
public static void Flicker(ILightshow lightshow)
|
||||||
|
{
|
||||||
|
if (cts != null)
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
cts = new();
|
||||||
|
_ = Task.Run(async () => await FlickerAsync(lightshow, cts.Token), cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async ValueTask FlickerAsync(ILightshow lightshow, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await foreach (var on in FlickerAnimationAsync())
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
byte brightness = on ? (byte)0xFF : (byte)0x00;
|
||||||
|
lightshow.SetBrightnessOverride(brightness);
|
||||||
|
}
|
||||||
|
lightshow.SetBrightnessOverride(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamps (in frames of 60 fps) of state switches, starting with 0 => ON.
|
||||||
|
private static readonly int[] MansionWallLampFlicker = [
|
||||||
|
0, 4, 8, 26, 28, 32, 37, 41, 42, 58, 60, 71,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static async IAsyncEnumerable<bool> FlickerAnimationAsync()
|
||||||
|
{
|
||||||
|
bool lastState = false;
|
||||||
|
int lastFrame = 0;
|
||||||
|
|
||||||
|
const int initialMillisecondsDelay = 4 * 1000 / 60;
|
||||||
|
await Task.Delay(initialMillisecondsDelay);
|
||||||
|
|
||||||
|
foreach (int frame in MansionWallLampFlicker)
|
||||||
|
{
|
||||||
|
// convert difference in frames into milliseconds
|
||||||
|
int millisecondsDelay = (int)((frame - lastFrame) / 60f * 1000f);
|
||||||
|
lastState = !lastState;
|
||||||
|
lastFrame = frame;
|
||||||
|
|
||||||
|
await Task.Delay(millisecondsDelay);
|
||||||
|
yield return lastState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
using HarmonyLib;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MuzikaGromche.Via;
|
||||||
|
|
||||||
|
class ViaBehaviour : MonoBehaviour
|
||||||
|
{
|
||||||
|
static ViaBehaviour? instance = null;
|
||||||
|
|
||||||
|
public static ViaBehaviour Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject("MuzikaGromche_ViaBehaviour", [
|
||||||
|
typeof(ViaBehaviour),
|
||||||
|
])
|
||||||
|
{
|
||||||
|
hideFlags = HideFlags.HideAndDontSave
|
||||||
|
};
|
||||||
|
DontDestroyOnLoad(go);
|
||||||
|
instance = go.GetComponent<ViaBehaviour>();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DevicePool<ViaDeviceDelegate> devicePool = null!;
|
||||||
|
DevicePoolLightshow<ViaDeviceDelegate> lightshow = null!;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
devicePool = new DevicePool<ViaDeviceDelegate>(new FrameworkDeviceFactory());
|
||||||
|
lightshow = new DevicePoolLightshow<ViaDeviceDelegate>(devicePool);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
devicePool.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetColor(Color color)
|
||||||
|
{
|
||||||
|
Color.RGBToHSV(color, out var hue, out var saturation, out var value);
|
||||||
|
|
||||||
|
var h = (byte)Mathf.RoundToInt(hue * 255);
|
||||||
|
var s = (byte)Mathf.RoundToInt(saturation * 255);
|
||||||
|
var v = (byte)Mathf.RoundToInt(value * 255);
|
||||||
|
|
||||||
|
lightshow.SetColor(h, s, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Restore()
|
||||||
|
{
|
||||||
|
devicePool.Restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Flicker()
|
||||||
|
{
|
||||||
|
Animations.Flicker(lightshow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(RoundManager))]
|
||||||
|
static class ViaFlickerLightsPatch
|
||||||
|
{
|
||||||
|
[HarmonyPatch(nameof(RoundManager.FlickerLights))]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
static void OnFlickerLights(RoundManager __instance)
|
||||||
|
{
|
||||||
|
var _ = __instance;
|
||||||
|
ViaBehaviour.Instance.Flicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using HidSharp;
|
||||||
|
|
||||||
|
namespace MuzikaGromche.Via;
|
||||||
|
|
||||||
|
enum ViaCommandId : byte
|
||||||
|
{
|
||||||
|
GetProtocolVersion = 0x01,
|
||||||
|
GetKeyboardValue = 0x02,
|
||||||
|
SetKeyboardValue = 0x03,
|
||||||
|
DynamicKeymapGetKeycode = 0x04,
|
||||||
|
DynamicKeymapSetKeycode = 0x05,
|
||||||
|
DynamicKeymapReset = 0x06,
|
||||||
|
CustomSetValue = 0x07,
|
||||||
|
CustomGetValue = 0x08,
|
||||||
|
CustomSave = 0x09,
|
||||||
|
EepromReset = 0x0A,
|
||||||
|
BootloaderJump = 0x0B,
|
||||||
|
DynamicKeymapMacroGetCount = 0x0C,
|
||||||
|
DynamicKeymapMacroGetBufferSize = 0x0D,
|
||||||
|
DynamicKeymapMacroGetBuffer = 0x0E,
|
||||||
|
DynamicKeymapMacroSetBuffer = 0x0F,
|
||||||
|
DynamicKeymapMacroReset = 0x10,
|
||||||
|
DynamicKeymapGetLayerCount = 0x11,
|
||||||
|
DynamicKeymapGetBuffer = 0x12,
|
||||||
|
DynamicKeymapSetBuffer = 0x13,
|
||||||
|
DynamicKeymapGetEncoder = 0x14,
|
||||||
|
DynamicKeymapSetEncoder = 0x15,
|
||||||
|
Unhandled = 0xFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ViaChannelId : byte
|
||||||
|
{
|
||||||
|
CustomChannel = 0,
|
||||||
|
QmkBacklightChannel = 1,
|
||||||
|
QmkRgblightChannel = 2,
|
||||||
|
QmkRgbMatrixChannel = 3,
|
||||||
|
QmkAudioChannel = 4,
|
||||||
|
QmkLedMatrixChannel = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ViaQmkRgbMatrixValue : byte
|
||||||
|
{
|
||||||
|
Brightness = 1,
|
||||||
|
Effect = 2,
|
||||||
|
EffectSpeed = 3,
|
||||||
|
Color = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ViaEffectMode : byte
|
||||||
|
{
|
||||||
|
Off = 0x00,
|
||||||
|
Static = 0x01,
|
||||||
|
Breathing = 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViaKeyboardApi
|
||||||
|
{
|
||||||
|
private const uint RAW_EPSIZE = 32;
|
||||||
|
private const byte COMMAND_START = 0x00;
|
||||||
|
|
||||||
|
private readonly HidStream stream;
|
||||||
|
|
||||||
|
public ViaKeyboardApi(HidStream stream)
|
||||||
|
{
|
||||||
|
this.stream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? HidCommand(ViaCommandId command, byte[] input)
|
||||||
|
{
|
||||||
|
byte[] commandBytes = [(byte)command, .. input];
|
||||||
|
|
||||||
|
if (!HidSend(commandBytes))
|
||||||
|
{
|
||||||
|
// Console.WriteLine($"no send");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = HidRead();
|
||||||
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
// Console.WriteLine($"no read");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console.WriteLine($"write command {commandBytes.BytesToHex()}");
|
||||||
|
// Console.WriteLine($"read buffer {buffer.BytesToHex()}");
|
||||||
|
if (!buffer.AsSpan(1).StartsWith(commandBytes))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buffer.AsSpan(1).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a raw HID command prefixed with the command byte and returns the response if successful.
|
||||||
|
/// </summary>
|
||||||
|
public async ValueTask<byte[]?> HidCommandAsync(ViaCommandId command, byte[] input, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
byte[] commandBytes = [(byte)command, .. input];
|
||||||
|
|
||||||
|
if (!await HidSendAsync(commandBytes, cancellationToken))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
// Console.WriteLine($"no send");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = await HidReadAsync(cancellationToken);
|
||||||
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
// Console.WriteLine($"no read");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console.WriteLine($"write command {commandBytes.BytesToHex()}");
|
||||||
|
// Console.WriteLine($"read buffer {buffer.BytesToHex()}");
|
||||||
|
if (!buffer.AsSpan(1).StartsWith(commandBytes))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buffer.AsSpan(1).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads from the HID device. Returns null if the read fails.
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? HidRead()
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[RAW_EPSIZE + 1];
|
||||||
|
// Console.WriteLine($"{buffer.BytesToHex()}");
|
||||||
|
int count = stream.Read(buffer);
|
||||||
|
if (count != RAW_EPSIZE + 1)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads from the HID device. Returns null if the read fails.
|
||||||
|
/// </summary>
|
||||||
|
public async ValueTask<byte[]?> HidReadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[RAW_EPSIZE + 1];
|
||||||
|
// Console.WriteLine($"{buffer.BytesToHex()}");
|
||||||
|
int count = await stream.ReadAsync(buffer, cancellationToken);
|
||||||
|
if (count != RAW_EPSIZE + 1)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
|
||||||
|
/// </summary>
|
||||||
|
public bool HidSend(byte[] bytes)
|
||||||
|
{
|
||||||
|
if (bytes.Length > RAW_EPSIZE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
byte[] commandBytes = [COMMAND_START, .. bytes];
|
||||||
|
byte[] paddedArray = new byte[RAW_EPSIZE + 1];
|
||||||
|
commandBytes.AsSpan().CopyTo(paddedArray);
|
||||||
|
// Console.WriteLine($"Send {paddedArray.BytesToHex()}");
|
||||||
|
stream.Write(paddedArray);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
|
||||||
|
/// </summary>
|
||||||
|
public async ValueTask<bool> HidSendAsync(byte[] bytes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (bytes.Length > RAW_EPSIZE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
byte[] commandBytes = [COMMAND_START, .. bytes];
|
||||||
|
byte[] paddedArray = new byte[RAW_EPSIZE + 1];
|
||||||
|
commandBytes.AsSpan().CopyTo(paddedArray);
|
||||||
|
// Console.WriteLine($"Send {paddedArray.BytesToHex()}");
|
||||||
|
await stream.WriteAsync(paddedArray, cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the protocol version of the keyboard.
|
||||||
|
/// </summary>
|
||||||
|
public ushort GetProtocolVersion()
|
||||||
|
{
|
||||||
|
var output = HidCommand(ViaCommandId.GetProtocolVersion, []);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (ushort)((output[1] << 8) | output[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the protocol version of the keyboard.
|
||||||
|
/// </summary>
|
||||||
|
public async ValueTask<ushort> GetProtocolVersionAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var output = await HidCommandAsync(ViaCommandId.GetProtocolVersion, [], cancellationToken);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (ushort)((output[1] << 8) | output[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte? GetRgbMatrixBrightness()
|
||||||
|
{
|
||||||
|
var output = HidCommand(ViaCommandId.CustomGetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Brightness,
|
||||||
|
]);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return output[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<byte?> GetRgbMatrixBrightnessAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Brightness,
|
||||||
|
], cancellationToken);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return output[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetRgbMatrixBrightness(byte brightness)
|
||||||
|
{
|
||||||
|
return HidCommand(ViaCommandId.CustomSetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Brightness,
|
||||||
|
brightness,
|
||||||
|
]) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> SetRgbMatrixBrightnessAsync(byte brightness, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await HidCommandAsync(ViaCommandId.CustomSetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Brightness,
|
||||||
|
brightness,
|
||||||
|
], cancellationToken) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte? GetRgbMatrixEffect()
|
||||||
|
{
|
||||||
|
var output = HidCommand(ViaCommandId.CustomGetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Effect,
|
||||||
|
]);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return output[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<byte?> GetRgbMatrixEffectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Effect,
|
||||||
|
], cancellationToken);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return output[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetRgbMatrixEffect(ViaEffectMode effect)
|
||||||
|
{
|
||||||
|
return SetRgbMatrixEffect((byte)effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetRgbMatrixEffect(byte effect)
|
||||||
|
{
|
||||||
|
return HidCommand(ViaCommandId.CustomSetValue, [
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Effect,
|
||||||
|
effect,
|
||||||
|
]) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> SetRgbMatrixEffectAsync(ViaEffectMode effect, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return SetRgbMatrixEffectAsync((byte)effect, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> SetRgbMatrixEffectAsync(byte effect, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await HidCommandAsync(ViaCommandId.CustomSetValue, [
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Effect,
|
||||||
|
effect,
|
||||||
|
], cancellationToken) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (byte Hue, byte Saturation)? GetRgbMatrixColor()
|
||||||
|
{
|
||||||
|
var output = HidCommand(ViaCommandId.CustomGetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Color,
|
||||||
|
]);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte hue = output[3];
|
||||||
|
byte saturation = output[4];
|
||||||
|
return (hue, saturation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<(byte Hue, byte Saturation)?> GetRgbMatrixColorAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
|
||||||
|
[
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Color,
|
||||||
|
], cancellationToken);
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte hue = output[3];
|
||||||
|
byte saturation = output[4];
|
||||||
|
return (hue, saturation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetRgbMatrixColor(byte hue, byte saturation)
|
||||||
|
{
|
||||||
|
return HidCommand(ViaCommandId.CustomSetValue, [
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Color,
|
||||||
|
hue, saturation,
|
||||||
|
]) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> SetRgbMatrixColorAsync(byte hue, byte saturation, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await HidCommandAsync(ViaCommandId.CustomSetValue, [
|
||||||
|
(byte)ViaChannelId.QmkRgbMatrixChannel,
|
||||||
|
(byte)ViaQmkRgbMatrixValue.Color,
|
||||||
|
hue, saturation,
|
||||||
|
], cancellationToken) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct HueSaturationColor(byte Hue, byte Saturation);
|
||||||
11
README.md
11
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
_Add some content to your Inverse teleporter experience on Titan!<sup>1</sup>_
|
_Add some content to your Inverse teleporter experience on Titan!<sup>1</sup>_
|
||||||
|
|
||||||
Muzika Gromche literally means _"crank music louder"_. This mod replaces Jester's winding up and chasing sounds with **a whole library** of timed to the beat and **seamlessly looped** popular energetic songs, combined with various **visual effects**. Song choice is random each time but **synchronized** with clients: everyone in the lobby will vibe to the same tunes, however **vanilla-compatible** client-side playback is also supported.
|
Muzika Gromche literally means _"crank music louder"_. This mod replaces Jester's winding up and chasing sounds with **a whole library** of timed to the beat and **seamlessly looped** popular energetic songs, combined with various **visual effects**. Song choice is random each day but **synchronized** with clients: everyone in the lobby will vibe to the same tunes, however **vanilla-compatible** client-side playback is also supported.
|
||||||
|
|
||||||
A demo video is worth a thousand words. Check out what Muzika Gromche does:
|
A demo video is worth a thousand words. Check out what Muzika Gromche does:
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ English playlist features artists such as **Imagine Dragons, Fall Out Boy, Bon J
|
||||||
|
|
||||||
Russian playlist includes **Би-2, Витас, Глюк’oZa** (Глюкоза) & **Ленинград, Дискотека Авария, Noize MC, Oxxxymiron, Сплин, Пошлая Молли.**
|
Russian playlist includes **Би-2, Витас, Глюк’oZa** (Глюкоза) & **Ленинград, Дискотека Авария, Noize MC, Oxxxymiron, Сплин, Пошлая Молли.**
|
||||||
|
|
||||||
There are also a K-pop tracks by **aespa** and **BTS**, and an anime opening from **One Punch Man.**
|
There are also a K-pop track by **aespa**, and an anime opening from **One Punch Man.**
|
||||||
|
|
||||||
Seasonal New Year's songs:
|
Seasonal New Year's songs:
|
||||||
|
|
||||||
|
|
@ -61,12 +61,9 @@ Any player can change the following 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.
|
||||||
|
|
||||||
See also [MuzikaGromche mod's release thread](https://discord.com/channels/1168655651455639582/1433881654866477318) at [Lethal Company Modding](https://discord.gg/XeyYqRdRGC) Discord server.
|
See also [mod's release thread](https://discord.com/channels/1168655651455639582/1433881654866477318) at [Lethal Company Modding](https://discord.gg/XeyYqRdRGC) Discord server (in case the invite link expires, there should be a fresh one at [lethal.wiki](https://lethal.wiki/)).
|
||||||
|
|
||||||
Also check out my other mods!
|
Also check out my other mod, 💭 [Hookah Place](https://thunderstore.io/c/lethal-company/p/Ratijas/HookahPlace/) ship decor/furniture! (thread: https://discord.com/channels/1169792572382773318/1465128348434038980)
|
||||||
|
|
||||||
- 💭 [Hookah Place — Ship decor/furniture](https://thunderstore.io/c/lethal-company/p/Ratijas/HookahPlace/)
|
|
||||||
- 6️⃣7️⃣ [Scrap Value Detector 67 — Scan and notify when a scrap worth 67 spawns on a map](https://thunderstore.io/c/lethal-company/p/Ratijas/ScrapValueDetector67/)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"BepInEx-BepInExPack-5.4.2100",
|
"BepInEx-BepInExPack-5.4.2100",
|
||||||
"AinaVT-LethalConfig-1.4.6",
|
"AinaVT-LethalConfig-1.4.6",
|
||||||
"WaterGun-V70PoweredLights_Fix-1.2.1"
|
"WaterGun-V70PoweredLights_Fix-1.1.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue