forked from nikita/muzika-gromche
Compare commits
18 Commits
f77e41bd17
...
c6b580af5a
| Author | SHA1 | Date |
|---|---|---|
|
|
c6b580af5a | |
|
|
47f42a5d49 | |
|
|
3cdd99f57e | |
|
|
8561834e22 | |
|
|
df0cfc16ff | |
|
|
b99e4c1e2c | |
|
|
dfefc06abf | |
|
|
702eb9fccd | |
|
|
28f63eff3d | |
|
|
7794e44831 | |
|
|
24bae0e370 | |
|
|
871c608a33 | |
|
|
a71c61b5c7 | |
|
|
f55e2e8436 | |
|
|
7fb63e4718 | |
|
|
d600a3170f | |
|
|
70bdcc7ecf | |
|
|
2eef12048d |
Binary file not shown.
Binary file not shown.
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,9 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## MuzikaGromche 1337.9001.69
|
||||
## MuzikaGromche 1337.9001.69 - Six Seven Edition
|
||||
|
||||
- Added support for v80 (also known as "v81").
|
||||
- Show real Artist & Song info in the config.
|
||||
- Integrated visual effects with QMK/VIA RGB keyboard (specifically input modules for Framework Laptop 16).
|
||||
- Added a new track Yesterday.
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<Project>
|
||||
<Target Name="NetcodePatch" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="dotnet netcode-patch -uv 2022.3.62 -nv 1.12.0 "$(TargetPath)" @(ReferencePathWithRefAssemblies->'"%(Identity)"', ' ')"/>
|
||||
<Exec Command="dotnet netcode-patch -uv 2022.3.62 -nv 1.12.2 "$(TargetPath)" @(ReferencePathWithRefAssemblies->'"%(Identity)"', ' ')"/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections;
|
||||
using GameNetcodeStuff;
|
||||
using HarmonyLib;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
|
@ -21,7 +22,12 @@ namespace MuzikaGromche
|
|||
SetTextImpl(text ?? GameOverTextModdedDefault);
|
||||
}
|
||||
|
||||
public static IEnumerator SetTextAndClear(string? text)
|
||||
public static void SetTextAndClear(string? text)
|
||||
{
|
||||
HUDManager.Instance.StartCoroutine(SetTextAndClearImpl(text));
|
||||
}
|
||||
|
||||
public static IEnumerator SetTextAndClearImpl(string? text)
|
||||
{
|
||||
SetText(text);
|
||||
// Game Over animation duration is about 4.25 seconds
|
||||
|
|
@ -60,7 +66,6 @@ namespace MuzikaGromche
|
|||
[HarmonyPatch(typeof(RoundManager))]
|
||||
static class DeathScreenGameOverTextResetPatch
|
||||
{
|
||||
[HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
|
||||
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
|
||||
[HarmonyPrefix]
|
||||
static void OnDestroy(RoundManager __instance)
|
||||
|
|
@ -69,4 +74,29 @@ namespace MuzikaGromche
|
|||
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,11 +113,17 @@ namespace MuzikaGromche
|
|||
|
||||
foreach (var discoBall in CachedDiscoBalls)
|
||||
{
|
||||
discoBall.SetActive(on);
|
||||
if (discoBall != null)
|
||||
{
|
||||
discoBall.SetActive(on);
|
||||
}
|
||||
}
|
||||
foreach (var animator in CachedDiscoBallAnimators)
|
||||
{
|
||||
animator?.SetBool("on", on);
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetBool("on", on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -899,6 +899,97 @@ public static class Library
|
|||
FadeOutDuration = 1.5f,
|
||||
FlickerLightsTimeSeries = [-33, 39],
|
||||
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(
|
||||
[-2f, -1f, 6f],
|
||||
|
|
@ -1118,5 +1209,34 @@ public static class Library
|
|||
[ 0f, 0.5f, 0f]),
|
||||
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,13 +47,15 @@
|
|||
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" Private="false" />
|
||||
<PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" />
|
||||
<PackageReference Include="TeamBMX.LobbyCompatibility" Version="1.*" PrivateAssets="all" Private="false" />
|
||||
<PackageReference Include="HidSharp" Version="2.6.4" PrivateAssets="all" Private="false" GeneratePathProperty="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Assembly-CSharp" Publicize="true" Private="false">
|
||||
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DunGen" Publicize="true" Private="false">
|
||||
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\DunGen.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Collections" Private="false">
|
||||
<HintPath>$(LethalCompanyDir)Lethal Company_Data\Managed\Unity.Collections.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
@ -90,9 +92,7 @@
|
|||
<PackagedResources Include="$(SolutionDir)icon.png" />
|
||||
<PackagedResources Include="$(SolutionDir)manifest.json" />
|
||||
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_discoball" />
|
||||
<PackagedResources Include="$(ProjectDir)UnityAssets\muzikagromche_poweredlightsanimators" />
|
||||
<PackagedResources Include="$(TargetDir)$(AssemblyName).dll" />
|
||||
<PackagedResources Include="$(PkgHidSharp)\lib\netstandard2.0\*.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -35,12 +35,8 @@ namespace MuzikaGromche
|
|||
|
||||
private static int GetCurrentSeed()
|
||||
{
|
||||
var seed = 0;
|
||||
var roundManager = RoundManager.Instance;
|
||||
if (roundManager != null && roundManager.dungeonGenerator != null)
|
||||
{
|
||||
seed = roundManager.dungeonGenerator.Generator.ChosenSeed;
|
||||
}
|
||||
var rng = new System.Random(unchecked((int)DateTime.Now.Ticks));
|
||||
var seed = rng.Next();
|
||||
return seed;
|
||||
}
|
||||
|
||||
|
|
@ -158,19 +154,17 @@ namespace MuzikaGromche
|
|||
#endif
|
||||
Config = new Config(base.Config);
|
||||
DiscoBallManager.Load();
|
||||
PoweredLightsAnimators.Load();
|
||||
Harmony = new Harmony(MyPluginInfo.PLUGIN_NAME);
|
||||
Harmony.PatchAll(typeof(GameNetworkManagerPatch));
|
||||
Harmony.PatchAll(typeof(JesterPatch));
|
||||
Harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch));
|
||||
Harmony.PatchAll(typeof(AllPoweredLightsPatch));
|
||||
Harmony.PatchAll(typeof(DiscoBallTilePatch));
|
||||
Harmony.PatchAll(typeof(DiscoBallDespawnPatch));
|
||||
Harmony.PatchAll(typeof(SpawnRatePatch));
|
||||
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
||||
Harmony.PatchAll(typeof(PlayerControllerBKillPlayerPatch));
|
||||
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
||||
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
||||
Harmony.PatchAll(typeof(Via.ViaFlickerLightsPatch));
|
||||
NetcodePatcher();
|
||||
Compatibility.Register(this);
|
||||
}
|
||||
|
|
@ -348,6 +342,8 @@ namespace MuzikaGromche
|
|||
|
||||
internal IAudioTrack[] GetTracks();
|
||||
|
||||
internal int Count() => GetTracks().Length;
|
||||
|
||||
// Index is a non-negative monotonically increasing number of times
|
||||
// this ISelectableTrack has been played for this Jester on this day.
|
||||
// A group of tracks can use this index to rotate tracks sequentially.
|
||||
|
|
@ -591,6 +587,8 @@ namespace MuzikaGromche
|
|||
|
||||
IAudioTrack[] ISelectableTrack.GetTracks() => [this];
|
||||
|
||||
int ISelectableTrack.Count() => 1;
|
||||
|
||||
IAudioTrack ISelectableTrack.SelectTrack(int index) => this;
|
||||
|
||||
void ISelectableTrack.Debug()
|
||||
|
|
@ -615,6 +613,8 @@ namespace MuzikaGromche
|
|||
|
||||
IAudioTrack[] ISelectableTrack.GetTracks() => Tracks;
|
||||
|
||||
int ISelectableTrack.Count() => Tracks.Length;
|
||||
|
||||
IAudioTrack ISelectableTrack.SelectTrack(int index)
|
||||
{
|
||||
if (Tracks.Length == 0)
|
||||
|
|
@ -1329,7 +1329,6 @@ namespace MuzikaGromche
|
|||
{
|
||||
// Calculate final color, substituting null with initialColor if needed.
|
||||
public abstract Color GetColor(Color initialColor);
|
||||
public abstract Color? GetNullableColor();
|
||||
|
||||
protected string NullableColorToString(Color? color)
|
||||
{
|
||||
|
|
@ -1346,11 +1345,6 @@ namespace MuzikaGromche
|
|||
return Color ?? initialColor;
|
||||
}
|
||||
|
||||
public override Color? GetNullableColor()
|
||||
{
|
||||
return Color;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Color(#{NullableColorToString(Color)})";
|
||||
|
|
@ -1372,7 +1366,7 @@ namespace MuzikaGromche
|
|||
return Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f));
|
||||
}
|
||||
|
||||
public override Color? GetNullableColor()
|
||||
private Color? GetNullableColor()
|
||||
{
|
||||
return From is { } from && To is { } to ? Color.Lerp(from, to, Mathf.Clamp(Easing.Eval(T), 0f, 1f)) : null;
|
||||
}
|
||||
|
|
@ -2052,11 +2046,6 @@ namespace MuzikaGromche
|
|||
const string IntroAudioGameObjectName = "MuzikaGromcheAudio (Intro)";
|
||||
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 BeatTimeState? BeatTimeState = null;
|
||||
internal AudioSource IntroAudioSource = null!;
|
||||
|
|
@ -2101,8 +2090,6 @@ namespace MuzikaGromche
|
|||
public override void OnDestroy()
|
||||
{
|
||||
Config.Volume.SettingChanged -= UpdateVolume;
|
||||
|
||||
DeathScreenGameOverTextManager.Clear();
|
||||
Stop();
|
||||
}
|
||||
|
||||
|
|
@ -2117,6 +2104,7 @@ namespace MuzikaGromche
|
|||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
HostIsModded = IsServer;
|
||||
ChooseTrackDeferred();
|
||||
foreach (var track in Plugin.Tracks)
|
||||
{
|
||||
|
|
@ -2147,7 +2135,8 @@ namespace MuzikaGromche
|
|||
|
||||
private void ChooseTrackDeferredDelegate(object sender, EventArgs e)
|
||||
{
|
||||
SelectedTrackIndex = 0;
|
||||
HostSelectableTrack = null;
|
||||
HostSelectableTrackIndex = 0;
|
||||
ChooseTrackDeferred();
|
||||
}
|
||||
|
||||
|
|
@ -2168,7 +2157,8 @@ 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.
|
||||
bool HostIsModded = false;
|
||||
// Initialized to `IsServer` on network spawn. If I am the host, then I am modded, otherwise we'll find out later.
|
||||
bool HostIsModded;
|
||||
|
||||
// Playing with modded host automatically disables vanilla compatability mode
|
||||
public bool VanillaCompatMode => IsServer ? Config.VanillaCompatMode : !HostIsModded;
|
||||
|
|
@ -2182,12 +2172,12 @@ namespace MuzikaGromche
|
|||
|
||||
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();
|
||||
}
|
||||
else if (IsServer)
|
||||
{
|
||||
ChooseTrackServerRpc();
|
||||
ChooseTrackOnServer();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -2204,7 +2194,7 @@ namespace MuzikaGromche
|
|||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
[Rpc(SendTo.Everyone)]
|
||||
void SetTrackClientRpc(string name)
|
||||
{
|
||||
Plugin.Log.LogDebug($"SetTrackClientRpc {name}");
|
||||
|
|
@ -2227,14 +2217,25 @@ namespace MuzikaGromche
|
|||
}
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
void ChooseTrackServerRpc()
|
||||
// Host selected this group of tracks, and
|
||||
private ISelectableTrack? HostSelectableTrack = null;
|
||||
|
||||
// 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()
|
||||
{
|
||||
var selectableTrack = Plugin.ChooseTrack();
|
||||
var audioTrack = selectableTrack.SelectTrack(SelectedTrackIndex);
|
||||
Plugin.Log.LogInfo($"ChooseTrackServerRpc {selectableTrack.Name} #{SelectedTrackIndex} {audioTrack.Name}");
|
||||
if (HostSelectableTrack == null || HostSelectableTrackIndex >= HostSelectableTrack.Count())
|
||||
{
|
||||
HostSelectableTrack = Plugin.ChooseTrack();
|
||||
HostSelectableTrackIndex = 0;
|
||||
}
|
||||
var audioTrack = HostSelectableTrack.SelectTrack(HostSelectableTrackIndex);
|
||||
Plugin.Log.LogInfo($"ChooseTrackOnServer {HostSelectableTrack.Name} #{HostSelectableTrackIndex} {audioTrack.Name}");
|
||||
SetTrackClientRpc(audioTrack.Name);
|
||||
SelectedTrackIndex += 1;
|
||||
HostSelectableTrackIndex += 1;
|
||||
}
|
||||
|
||||
void ChooseTrackCompat()
|
||||
|
|
@ -2347,7 +2348,6 @@ namespace MuzikaGromche
|
|||
internal void Stop()
|
||||
{
|
||||
PoweredLightsBehaviour.Instance.ResetLightColor();
|
||||
Via.ViaBehaviour.Instance.Restore();
|
||||
DiscoBallManager.Disable();
|
||||
ScreenFiltersManager.Clear();
|
||||
|
||||
|
|
@ -2377,7 +2377,9 @@ namespace MuzikaGromche
|
|||
// Just in case if players have spawned multiple Jesters,
|
||||
// Don't reset Config.CurrentTrack to null,
|
||||
// so that the latest chosen track remains set.
|
||||
CurrentTrack = null;
|
||||
// Don't reset MuzikaGromcheJesterNetworkBehaviour.CurrentTrack to null,
|
||||
// because it may have already been set by host via RPC.
|
||||
// CurrentTrack = null;
|
||||
}
|
||||
|
||||
public void OverrideDeathScreenGameOverText()
|
||||
|
|
@ -2387,7 +2389,7 @@ namespace MuzikaGromche
|
|||
// Playing as a client with a host who doesn't have the mod
|
||||
return;
|
||||
}
|
||||
StartCoroutine(DeathScreenGameOverTextManager.SetTextAndClear(CurrentTrack.GameOverText));
|
||||
DeathScreenGameOverTextManager.SetTextAndClear(CurrentTrack.GameOverText);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2557,18 +2559,9 @@ namespace MuzikaGromche
|
|||
|
||||
case SetLightsColorEvent e:
|
||||
PoweredLightsBehaviour.Instance.SetLightColor(e);
|
||||
if (localPlayerCanHearMusic && e.GetNullableColor() is { } color)
|
||||
{
|
||||
Via.ViaBehaviour.Instance.SetColor(color);
|
||||
}
|
||||
else
|
||||
{
|
||||
Via.ViaBehaviour.Instance.Restore();
|
||||
}
|
||||
break;
|
||||
|
||||
case FlickerLightsEvent:
|
||||
// VIA is handled by a Harmony patch to integrate with all flickering events, not just from this mod.
|
||||
RoundManager.Instance.FlickerLights(true);
|
||||
break;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,245 +1,9 @@
|
|||
using DunGen;
|
||||
using HarmonyLib;
|
||||
using MuzikaGromche.Via;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MuzikaGromche
|
||||
{
|
||||
static class PoweredLightsAnimators
|
||||
{
|
||||
private const string PoweredLightTag = "PoweredLight";
|
||||
|
||||
private delegate void ManualPatch(GameObject animatorContainer);
|
||||
|
||||
private readonly record struct AnimatorPatch(
|
||||
string AnimatorContainerPath,
|
||||
RuntimeAnimatorController AnimatorController,
|
||||
bool AddTagAndAnimator,
|
||||
ManualPatch? ManualPatch);
|
||||
|
||||
private readonly record struct TilePatch(string TileName, AnimatorPatch[] Patches)
|
||||
{
|
||||
// We are specifically looking for cloned tiles, not the original prototypes.
|
||||
public readonly string TileCloneName = $"{TileName}(Clone)";
|
||||
}
|
||||
|
||||
private readonly record struct AnimatorPatchDescriptor(
|
||||
string AnimatorContainerPath,
|
||||
string AnimatorControllerAssetPath,
|
||||
bool AddTagAndAnimator = false,
|
||||
ManualPatch? ManualPatch = null)
|
||||
{
|
||||
public AnimatorPatch Load(AssetBundle assetBundle)
|
||||
{
|
||||
var animationController = assetBundle.LoadAsset<RuntimeAnimatorController>(AnimatorControllerAssetPath)
|
||||
?? throw new FileNotFoundException($"RuntimeAnimatorController not found: {AnimatorControllerAssetPath}", AnimatorControllerAssetPath);
|
||||
return new(AnimatorContainerPath, animationController, AddTagAndAnimator, ManualPatch);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct TilePatchDescriptor(string[] TileNames, AnimatorPatchDescriptor[] Descriptors)
|
||||
{
|
||||
public TilePatchDescriptor(string TileName, AnimatorPatchDescriptor[] Descriptors)
|
||||
: this([TileName], Descriptors)
|
||||
{
|
||||
}
|
||||
|
||||
public TilePatch[] Load(AssetBundle assetBundle)
|
||||
{
|
||||
var patches = Descriptors.Select(d => d.Load(assetBundle)).ToArray();
|
||||
return [.. TileNames.Select(tileName => new TilePatch(tileName, patches))];
|
||||
}
|
||||
}
|
||||
|
||||
private static IDictionary<string, TilePatch> Patches = new Dictionary<string, TilePatch>();
|
||||
|
||||
private static AudioClip AudioClipOn = null!;
|
||||
private static AudioClip AudioClipOff = null!;
|
||||
private static AudioClip AudioClipFlicker = null!;
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
const string BundleFileName = "muzikagromche_poweredlightsanimators";
|
||||
string bundlePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), BundleFileName);
|
||||
var assetBundle = AssetBundle.LoadFromFile(bundlePath)
|
||||
?? throw new NullReferenceException("Failed to load bundle");
|
||||
|
||||
AudioClipOn = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOn.ogg");
|
||||
AudioClipOff = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightOff.ogg");
|
||||
AudioClipFlicker = assetBundle.LoadAsset<AudioClip>("Assets/LethalCompany/Mods/MuzikaGromche/AudioClips/LightFlicker.ogg");
|
||||
|
||||
const string BasePath = "Assets/LethalCompany/Mods/MuzikaGromche/AnimatorControllers/";
|
||||
|
||||
const string PointLight4 = $"{BasePath}Point Light (4) (Patched).controller";
|
||||
const string MineshaftSpotlight = $"{BasePath}MineshaftSpotlight (Patched).controller";
|
||||
const string LightbulbsLine = $"{BasePath}lightbulbsLineMesh (Patched).controller";
|
||||
const string CeilingFan = $"{BasePath}CeilingFan (originally GameObject) (Patched).controller";
|
||||
const string LEDHangingLight = $"{BasePath}LEDHangingLight (Patched).controller";
|
||||
const string MineshaftStartTileSpotlight = $"{BasePath}MineshaftStartTileSpotlight (New).controller";
|
||||
|
||||
TilePatchDescriptor[] descriptors =
|
||||
[
|
||||
// any version
|
||||
new("KitchenTile", [
|
||||
new("PoweredLightTypeB", PointLight4),
|
||||
new("PoweredLightTypeB (1)", PointLight4),
|
||||
]),
|
||||
// < v70
|
||||
new("ManorStartRoom", [
|
||||
new("ManorStartRoom/Chandelier/PoweredLightTypeB (1)", PointLight4),
|
||||
new("ManorStartRoom/Chandelier2/PoweredLightTypeB", PointLight4),
|
||||
]),
|
||||
// v70+
|
||||
new("ManorStartRoomSmall", [
|
||||
new("ManorStartRoomMesh/Chandelier/PoweredLightTypeB (1)", PointLight4),
|
||||
new("ManorStartRoomMesh/Chandelier2/PoweredLightTypeB", PointLight4),
|
||||
]),
|
||||
new("NarrowHallwayTile2x2", [
|
||||
new("MineshaftSpotlight (1)", MineshaftSpotlight),
|
||||
new("MineshaftSpotlight (2)", MineshaftSpotlight),
|
||||
]),
|
||||
new("BirthdayRoomTile", [
|
||||
new("Lights/MineshaftSpotlight", MineshaftSpotlight),
|
||||
]),
|
||||
new("BathroomTileContainer", [
|
||||
new("MineshaftSpotlight", MineshaftSpotlight),
|
||||
new("LightbulbLine/lightbulbsLineMesh", LightbulbsLine),
|
||||
]),
|
||||
new(["BedroomTile", "BedroomTileB"], [
|
||||
new("CeilingFanAnimContainer", CeilingFan),
|
||||
new("MineshaftSpotlight (1)", MineshaftSpotlight),
|
||||
]),
|
||||
new("GarageTile", [
|
||||
new("HangingLEDBarLight (3)", LEDHangingLight),
|
||||
new("HangingLEDBarLight (4)", LEDHangingLight),
|
||||
// This HangingLEDBarLight's IndirectLight is wrongly named, so animator couldn't find it
|
||||
// renamed by WaterGun-V70PoweredLights_Fix-1.1.0
|
||||
// ManualPatch: RenameGameObjectPatch("IndirectLight (1)", "IndirectLight")),
|
||||
]),
|
||||
new("PoolTile", [
|
||||
new("PoolLights/HangingLEDBarLight", LEDHangingLight),
|
||||
new("PoolLights/HangingLEDBarLight (4)", LEDHangingLight),
|
||||
new("PoolLights/HangingLEDBarLight (5)", LEDHangingLight),
|
||||
]),
|
||||
new("MineshaftStartTile", [
|
||||
new("Cylinder.001 (1)", MineshaftStartTileSpotlight, AddTagAndAnimator: true),
|
||||
new("Cylinder.001 (2)", MineshaftStartTileSpotlight, AddTagAndAnimator: true),
|
||||
]),
|
||||
];
|
||||
|
||||
Patches = descriptors
|
||||
.SelectMany(d => d.Load(assetBundle))
|
||||
.ToDictionary(d => d.TileCloneName, d => d);
|
||||
}
|
||||
|
||||
public static void Patch(Tile tile)
|
||||
{
|
||||
if (tile == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tile));
|
||||
}
|
||||
|
||||
if (Patches.TryGetValue(tile.gameObject.name, out var tilePatch))
|
||||
{
|
||||
foreach (var patch in tilePatch.Patches)
|
||||
{
|
||||
Transform animationContainerTransform = tile.gameObject.transform.Find(patch.AnimatorContainerPath);
|
||||
if (animationContainerTransform == null)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new NullReferenceException($"{tilePatch.TileName}/{patch.AnimatorContainerPath} Animation Container not found");
|
||||
#endif
|
||||
#pragma warning disable CS0162 // Unreachable code detected
|
||||
continue;
|
||||
#pragma warning restore CS0162 // Unreachable code detected
|
||||
}
|
||||
|
||||
GameObject animationContainer = animationContainerTransform.gameObject;
|
||||
Animator animator = animationContainer.GetComponent<Animator>();
|
||||
|
||||
if (patch.AddTagAndAnimator)
|
||||
{
|
||||
animationContainer.tag = PoweredLightTag;
|
||||
if (animator == null)
|
||||
{
|
||||
animator = animationContainer.AddComponent<Animator>();
|
||||
}
|
||||
if (!animationContainer.TryGetComponent<PlayAudioAnimationEvent>(out var audioScript))
|
||||
{
|
||||
audioScript = animationContainer.AddComponent<PlayAudioAnimationEvent>();
|
||||
audioScript.audioClip = AudioClipOn;
|
||||
audioScript.audioClip2 = AudioClipOff;
|
||||
audioScript.audioClip3 = AudioClipFlicker;
|
||||
}
|
||||
if (!animationContainer.TryGetComponent<AudioSource>(out var audioSource))
|
||||
{
|
||||
// Copy from an existing AudioSource of another light animator
|
||||
var otherSource = tile.gameObject.GetComponentInChildren<AudioSource>();
|
||||
if (otherSource != null)
|
||||
{
|
||||
audioSource = animationContainer.AddComponent<AudioSource>();
|
||||
audioSource.spatialBlend = 1;
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.outputAudioMixerGroup = otherSource.outputAudioMixerGroup;
|
||||
audioSource.spread = otherSource.spread;
|
||||
audioSource.rolloffMode = otherSource.rolloffMode;
|
||||
audioSource.maxDistance = otherSource.maxDistance;
|
||||
audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, otherSource.GetCustomCurve(AudioSourceCurveType.CustomRolloff));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (animator == null)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new NullReferenceException($"{tilePatch.TileName}/{patch.AnimatorContainerPath} Animation Component not found");
|
||||
#endif
|
||||
#pragma warning disable CS0162 // Unreachable code detected
|
||||
continue;
|
||||
#pragma warning restore CS0162 // Unreachable code detected
|
||||
}
|
||||
|
||||
patch.ManualPatch?.Invoke(animationContainer);
|
||||
|
||||
animator.runtimeAnimatorController = patch.AnimatorController;
|
||||
Plugin.Log.LogDebug($"{nameof(PoweredLightsAnimatorsPatch)} {tilePatch.TileName}/{patch.AnimatorContainerPath}: Replaced animator controller");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ManualPatch RenameGameObjectPatch(string relativePath, string newName) => animatorContainer =>
|
||||
{
|
||||
var targetObject = animatorContainer.transform.Find(relativePath)?.gameObject;
|
||||
if (targetObject == null)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new NullReferenceException($"{animatorContainer.name}/{relativePath}: GameObject not found!");
|
||||
#endif
|
||||
#pragma warning disable CS0162 // Unreachable code detected
|
||||
return;
|
||||
#pragma warning restore CS0162 // Unreachable code detected
|
||||
}
|
||||
targetObject.name = newName;
|
||||
Plugin.Log.LogDebug($"{nameof(PoweredLightsAnimatorsPatch)} {animatorContainer.name}/{relativePath}: Renamed GameObject");
|
||||
};
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Tile))]
|
||||
static class PoweredLightsAnimatorsPatch
|
||||
{
|
||||
[HarmonyPatch(nameof(Tile.AddTriggerVolume))]
|
||||
[HarmonyPostfix]
|
||||
static void OnAddTriggerVolume(Tile __instance)
|
||||
{
|
||||
PoweredLightsAnimators.Patch(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(RoundManager))]
|
||||
static class AllPoweredLightsPatch
|
||||
{
|
||||
|
|
@ -250,47 +14,11 @@ namespace MuzikaGromche
|
|||
// - (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)
|
||||
[HarmonyPostfix]
|
||||
static void OnRefreshLightsList(RoundManager __instance)
|
||||
{
|
||||
RefreshLightsListPatched(__instance);
|
||||
|
||||
var behaviour = __instance.gameObject.GetComponent<PoweredLightsBehaviour>() ?? __instance.gameObject.AddComponent<PoweredLightsBehaviour>();
|
||||
behaviour.Refresh();
|
||||
|
||||
// Skip the original method
|
||||
return false;
|
||||
}
|
||||
|
||||
static void RefreshLightsListPatched(RoundManager self)
|
||||
{
|
||||
// Reusable list to reduce allocations
|
||||
List<Light> lights = [];
|
||||
|
||||
self.allPoweredLights.Clear();
|
||||
self.allPoweredLightsAnimators.Clear();
|
||||
|
||||
GameObject[] gameObjects = GameObject.FindGameObjectsWithTag("PoweredLight");
|
||||
if (gameObjects == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var gameObject in gameObjects)
|
||||
{
|
||||
Animator animator = gameObject.GetComponentInChildren<Animator>();
|
||||
if (!(animator == null))
|
||||
{
|
||||
self.allPoweredLightsAnimators.Add(animator);
|
||||
// Patched section: Use list instead of singular GetComponentInChildren<Light>
|
||||
gameObject.GetComponentsInChildren(includeInactive: true, lights);
|
||||
self.allPoweredLights.AddRange(lights);
|
||||
}
|
||||
}
|
||||
foreach (var animator in self.allPoweredLightsAnimators)
|
||||
{
|
||||
animator.SetFloat("flickerSpeed", UnityEngine.Random.Range(0.6f, 1.4f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +68,11 @@ namespace MuzikaGromche
|
|||
{
|
||||
foreach (var data in AllPoweredLights)
|
||||
{
|
||||
data.Light.color = data.InitialColor;
|
||||
var light = data.Light;
|
||||
if (light != null)
|
||||
{
|
||||
light.color = data.InitialColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ namespace MuzikaGromche
|
|||
{
|
||||
drunkness = 0f;
|
||||
// Only the stop animation if vanilla doesn't animate TZP right now.
|
||||
if (GameNetworkManager.Instance.localPlayerController.drunkness == 0f)
|
||||
// Objects may be null on shutdown
|
||||
var network = GameNetworkManager.Instance;
|
||||
if (network != null && network.localPlayerController != null && network.localPlayerController.drunkness == 0f)
|
||||
{
|
||||
HUDManager.Instance.gasHelmetAnimator.SetBool("gasEmitting", value: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ namespace MuzikaGromche
|
|||
{
|
||||
if (EnemyIndex is int index)
|
||||
{
|
||||
if (__instance.EnemyCannotBeSpawned(index))
|
||||
if (__instance.InsideEnemyCannotBeSpawned(index, __instance.currentEnemyPower))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,83 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
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);
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,374 +0,0 @@
|
|||
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>_
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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, Сплин, Пошлая Молли.**
|
||||
|
||||
There are also a K-pop track by **aespa**, and an anime opening from **One Punch Man.**
|
||||
There are also a K-pop tracks by **aespa** and **BTS**, and an anime opening from **One Punch Man.**
|
||||
|
||||
Seasonal New Year's songs:
|
||||
|
||||
|
|
@ -61,9 +61,12 @@ Any player can change the following personal preferences locally.
|
|||
- [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.
|
||||
|
||||
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/)).
|
||||
See also [MuzikaGromche mod's release thread](https://discord.com/channels/1168655651455639582/1433881654866477318) at [Lethal Company Modding](https://discord.gg/XeyYqRdRGC) Discord server.
|
||||
|
||||
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)
|
||||
Also check out my other mods!
|
||||
|
||||
- 💭 [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": [
|
||||
"BepInEx-BepInExPack-5.4.2100",
|
||||
"AinaVT-LethalConfig-1.4.6",
|
||||
"WaterGun-V70PoweredLights_Fix-1.1.0"
|
||||
"WaterGun-V70PoweredLights_Fix-1.2.1"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue