Compare commits

..

9 Commits

Author SHA1 Message Date
ivan tkachenko 8c660e803c add new track Peretasovka 2025-06-17 18:14:29 +03:00
ivan tkachenko 44e0db2ba9 add new track RiseAndShine 2025-06-17 17:38:19 +03:00
ivan tkachenko cb85bcb72c add new track GodMode 2025-06-17 16:26:44 +03:00
ivan tkachenko e2ae6873b8 port MuzikaGromche to ogg format 2025-06-17 16:23:01 +03:00
ivan tkachenko 0b2b8992a5 add support for wav and ogg/vorbis audio files 2025-06-17 16:20:54 +03:00
ivan tkachenko ceb11e36f4 organize code into functions, add some comments, add a null check 2025-06-17 16:10:46 +03:00
ivan tkachenko 566bc0993e convert indentation to tabs 2025-06-17 15:52:10 +03:00
ivan tkachenko df4418b040 Add README, icon and manifest from Thunderstore bundle 2025-06-16 21:43:58 +03:00
ivan tkachenko ec18c12aa8 add more things to gitignore 2025-06-16 21:43:10 +03:00
16 changed files with 227 additions and 134 deletions

3
.gitignore vendored
View File

@ -5,4 +5,7 @@ riderModule.iml
/_ReSharper.Caches/ /_ReSharper.Caches/
.idea/ .idea/
*.dll *.dll
.vs/
dist/
MuzikaGromche.sln.DotSettings.user MuzikaGromche.sln.DotSettings.user
MuzikaGromche.zip

View File

@ -1 +1,3 @@
*.mp3 filter=lfs diff=lfs merge=lfs -text *.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -10,20 +10,21 @@ using UnityEngine.Networking;
namespace MuzikaGromche namespace MuzikaGromche
{ {
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin public class Plugin : BaseUnityPlugin
{ {
public static Track[] Tracks = [ public static Track[] Tracks = [
new Track new Track
{ {
Name = "MuzikaGromche", Name = "MuzikaGromche",
WindUpTimer = 46.3f, WindUpTimer = 46.3f,
Bpm = 130f, Bpm = 130f,
AudioType = AudioType.OGGVORBIS,
}, },
new Track new Track
{ {
Name = "VseVZale", Name = "VseVZale",
WindUpTimer = 39f, WindUpTimer = 39f,
Bpm = 138f, Bpm = 138f,
}, },
new Track new Track
@ -49,20 +50,89 @@ namespace MuzikaGromche
Name = "Durochka", Name = "Durochka",
WindUpTimer = 37f, WindUpTimer = 37f,
Bpm = 130f, Bpm = 130f,
} },
new Track
{
Name = "GodMode",
WindUpTimer = 40.38f,
Bpm = 108f,
AudioType = AudioType.OGGVORBIS,
},
new Track
{
Name = "RiseAndShine",
WindUpTimer = 59.87f,
Bpm = 137.36f,
AudioType = AudioType.OGGVORBIS,
},
new Track
{
Name = "Peretasovka",
WindUpTimer = 59.04f,
Bpm = 130f,
AudioType = AudioType.OGGVORBIS,
},
]; ];
public static Coroutine JesterLightSwitching; public static Coroutine JesterLightSwitching;
public static Track CurrentTrack; public static Track CurrentTrack;
public static void StartLightSwitching(MonoBehaviour __instance)
{
StopLightSwitching(__instance);
JesterLightSwitching = __instance.StartCoroutine(rotateColors());
}
public static void StopLightSwitching(MonoBehaviour __instance)
{
if (JesterLightSwitching != null) {
__instance.StopCoroutine(JesterLightSwitching);
JesterLightSwitching = null;
}
}
public static void SetLightColor(Color color)
{
foreach (var light in RoundManager.Instance.allPoweredLights)
{
light.color = color;
}
}
public static void ResetLightColor()
{
SetLightColor(Color.white);
}
// TODO: Move to Track class to make them customizable per-song
static List<Color> colors = [Color.magenta, Color.cyan, Color.green, Color.yellow];
public static IEnumerator rotateColors()
{
Debug.Log("Starting color rotation");
var i = 0;
while (true)
{
var color = colors[i];
Debug.Log("Chose color " + color);
SetLightColor(color);
i = (i + 1) % colors.Count;
if (CurrentTrack != null) {
yield return new WaitForSeconds(60f / CurrentTrack.Bpm);
} else {
yield break;
}
}
}
private void Awake() private void Awake()
{ {
string text = Info.Location.TrimEnd((PluginInfo.PLUGIN_NAME + ".dll").ToCharArray()); string text = Info.Location.TrimEnd((PluginInfo.PLUGIN_NAME + ".dll").ToCharArray());
UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2];
for (int i = 0; i < Tracks.Length; i++) { for (int i = 0; i < Tracks.Length; i++) {
Track track = Tracks[i]; Track track = Tracks[i];
requests[i * 2] = UnityWebRequestMultimedia.GetAudioClip($"File://{text}{track.Name}Start.mp3", AudioType.MPEG); requests[i * 2] = UnityWebRequestMultimedia.GetAudioClip($"File://{text}{track.FileNameStart}", track.AudioType);
requests[i * 2 + 1] = UnityWebRequestMultimedia.GetAudioClip($"File://{text}{track.Name}Loop.mp3", AudioType.MPEG); requests[i * 2 + 1] = UnityWebRequestMultimedia.GetAudioClip($"File://{text}{track.FileNameLoop}", track.AudioType);
requests[i * 2].SendWebRequest(); requests[i * 2].SendWebRequest();
requests[i * 2 + 1].SendWebRequest(); requests[i * 2 + 1].SendWebRequest();
} }
@ -71,141 +141,130 @@ namespace MuzikaGromche
if (requests.All(request => request.result == UnityWebRequest.Result.Success)) { if (requests.All(request => request.result == UnityWebRequest.Result.Success)) {
for (int i = 0; i < Tracks.Length; i++) { for (int i = 0; i < Tracks.Length; i++) {
Tracks[i].LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]); Track track = Tracks[i];
Tracks[i].LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]);
track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]);
} }
new Harmony(PluginInfo.PLUGIN_NAME).PatchAll(typeof(JesterPatch)); new Harmony(PluginInfo.PLUGIN_NAME).PatchAll(typeof(JesterPatch));
} else { } else {
Logger.LogError("Could not load audio file"); Logger.LogError("Could not load audio file");
} }
} }
} }
public class Track public class Track
{ {
public string Name; public string Name;
public float WindUpTimer; // Wind-up time can be shorter than the Start audio track, so that
public float Bpm; // the "pop" effect can be baked in the Start audio and kept away
public AudioClip LoadedStart; // from the looped part.
public AudioClip LoadedLoop; public float WindUpTimer;
} // BPM for light switching in sync with the music. There is no offset,
// so the Loop track should start precisely on a beat.
public float Bpm;
[HarmonyPatch(typeof(JesterAI))] // MPEG is basically mp3, and it can produce gaps at the start.
internal class JesterPatch // WAV is OK, but takes a lot of space. Try OGGVORBIS instead.
{ public AudioType AudioType = AudioType.MPEG;
[HarmonyPatch("Update")]
[HarmonyPrefix]
public static void DoNotStopTheMusicPrefix(JesterAI __instance, out State __state)
{
__state = new State();
__state.prevStateindex = __instance.previousState;
if (__instance.currentBehaviourStateIndex == 2 && __instance.previousBehaviourStateIndex != 2) {
// if just popped out
// then override farAudio so that vanilla logic does not stop the music
__state.farAudio = __instance.farAudio;
__instance.farAudio = __instance.creatureVoice;
}
}
static List<Color> colors = [Color.magenta, Color.cyan, Color.green, Color.yellow];
public static IEnumerator rotateColors() public AudioClip LoadedStart;
{ public AudioClip LoadedLoop;
Debug.Log("Starting color rotation");
var i = 0;
while (true)
{
var color = colors[i];
Debug.Log("Chose color " + color);
foreach (var light in RoundManager.Instance.allPoweredLights)
{
light.color = color;
}
i += 1; public string FileNameStart => $"{Name}Start.{ext}";
if (i >= colors.Count) i = 0; public string FileNameLoop => $"{Name}Loop.{ext}";
yield return new WaitForSeconds(60f / Plugin.CurrentTrack.Bpm); private string ext => AudioType switch
} {
} AudioType.MPEG => "mp3",
AudioType.WAV => "wav",
AudioType.OGGVORBIS => "ogg",
_ => "",
};
}
[HarmonyPatch(typeof(JesterAI))]
internal class JesterPatch
{
[HarmonyPatch("Update")]
[HarmonyPrefix]
public static void DoNotStopTheMusicPrefix(JesterAI __instance, out State __state)
{
__state = new State();
__state.prevStateindex = __instance.previousState;
if (__instance.currentBehaviourStateIndex == 2 && __instance.previousBehaviourStateIndex != 2) {
// if just popped out
// then override farAudio so that vanilla logic does not stop the music
__state.farAudio = __instance.farAudio;
__instance.farAudio = __instance.creatureVoice;
}
}
[HarmonyPatch("Update")] [HarmonyPatch("Update")]
[HarmonyPostfix] [HarmonyPostfix]
public static void DoNotStopTheMusic(JesterAI __instance, State __state) public static void DoNotStopTheMusic(JesterAI __instance, State __state)
{ {
if (__state.farAudio != null) if (__state.farAudio != null)
{ {
__instance.farAudio = __state.farAudio; __instance.farAudio = __state.farAudio;
} }
if (__instance.currentBehaviourStateIndex is 1 && __state.prevStateindex != 1)
{
// if just started winding up
// then stop the default music...
__instance.farAudio.Stop();
__instance.creatureVoice.Stop();
// ...and start modded music
var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed;
var sha = SHA256.Create();
var hash = sha.ComputeHash(BitConverter.GetBytes(seed));
var trackId = 0;
foreach (var t in hash)
{
// modulus division on byte array
trackId *= 256 % Plugin.Tracks.Length;
trackId %= Plugin.Tracks.Length;
trackId += t % Plugin.Tracks.Length;
trackId %= Plugin.Tracks.Length;
}
Debug.Log($"Seed is {seed}, chosen track is {trackId} out of {Plugin.Tracks.Length} tracks");
Plugin.CurrentTrack = Plugin.Tracks[trackId];
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
__instance.farAudio.maxDistance = 150;
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedStart;
__instance.farAudio.loop = false;
Debug.Log($"Playing start music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}");
__instance.farAudio.Play();
}
if (__instance.currentBehaviourStateIndex is 2 && __state.prevStateindex != 2) if (__instance.currentBehaviourStateIndex is 1 && __state.prevStateindex != 1)
{ {
__instance.creatureVoice.Stop(); // if just started winding up
// then stop the default music...
if (Plugin.JesterLightSwitching != null) { __instance.farAudio.Stop();
__instance.StopCoroutine(Plugin.JesterLightSwitching); __instance.creatureVoice.Stop();
Plugin.JesterLightSwitching = null;
}
Plugin.JesterLightSwitching = __instance.StartCoroutine(rotateColors());
}
if (__instance.currentBehaviourStateIndex != 2 && __state.prevStateindex == 2) // ...and start modded music
{ var seed = RoundManager.Instance.dungeonGenerator.Generator.ChosenSeed;
if (Plugin.JesterLightSwitching != null) { var sha = SHA256.Create();
__instance.StopCoroutine(Plugin.JesterLightSwitching); var hash = sha.ComputeHash(BitConverter.GetBytes(seed));
Plugin.JesterLightSwitching = null; var trackId = 0;
} foreach (var t in hash)
foreach (var light in RoundManager.Instance.allPoweredLights) {
{ // modulus division on byte array
light.color = Color.white; trackId *= 256 % Plugin.Tracks.Length;
} trackId %= Plugin.Tracks.Length;
} trackId += t % Plugin.Tracks.Length;
trackId %= Plugin.Tracks.Length;
}
Debug.Log($"Seed is {seed}, chosen track is {trackId} out of {Plugin.Tracks.Length} tracks");
Plugin.CurrentTrack = Plugin.Tracks[trackId];
__instance.popUpTimer = Plugin.CurrentTrack.WindUpTimer;
__instance.farAudio.maxDistance = 150;
__instance.farAudio.clip = Plugin.CurrentTrack.LoadedStart;
__instance.farAudio.loop = false;
Debug.Log($"Playing start music: maxDistance: {__instance.farAudio.maxDistance}, minDistance: {__instance.farAudio.minDistance}, volume: {__instance.farAudio.volume}, spread: {__instance.farAudio.spread}");
__instance.farAudio.Play();
}
if (__instance.currentBehaviourStateIndex is 2 && !__instance.creatureVoice.isPlaying) if (__instance.currentBehaviourStateIndex is 2 && __state.prevStateindex != 2)
{ {
__instance.creatureVoice.maxDistance = 150; __instance.creatureVoice.Stop();
__instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop; Plugin.StartLightSwitching(__instance);
var time = __instance.farAudio.time; }
var delay = Plugin.CurrentTrack.LoadedStart.length - time;
Debug.Log($"Start length: {Plugin.CurrentTrack.LoadedStart.length}; played time: {time}");
Debug.Log($"Playing loop music: maxDistance: {__instance.creatureVoice.maxDistance}, minDistance: {__instance.creatureVoice.minDistance}, volume: {__instance.creatureVoice.volume}, spread: {__instance.creatureVoice.spread}, in seconds: {delay}");
__instance.creatureVoice.PlayDelayed(delay);
}
}
}
internal class State if (__instance.currentBehaviourStateIndex != 2 && __state.prevStateindex == 2)
{ {
public AudioSource farAudio; Plugin.StopLightSwitching(__instance);
public int prevStateindex; Plugin.ResetLightColor();
} }
if (__instance.currentBehaviourStateIndex is 2 && !__instance.creatureVoice.isPlaying)
{
__instance.creatureVoice.maxDistance = 150;
__instance.creatureVoice.clip = Plugin.CurrentTrack.LoadedLoop;
var time = __instance.farAudio.time;
var delay = Plugin.CurrentTrack.LoadedStart.length - time;
Debug.Log($"Start length: {Plugin.CurrentTrack.LoadedStart.length}; played time: {time}");
Debug.Log($"Playing loop music: maxDistance: {__instance.creatureVoice.maxDistance}, minDistance: {__instance.creatureVoice.minDistance}, volume: {__instance.creatureVoice.volume}, spread: {__instance.creatureVoice.spread}, in seconds: {delay}");
__instance.creatureVoice.PlayDelayed(delay);
}
}
}
internal class State
{
public AudioSource farAudio;
public int prevStateindex;
}
} }

1
README.md Normal file
View File

@ -0,0 +1 @@
Adds some content to your reverse teleports on Titan

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

10
manifest.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "MuzikaGromche",
"version_number": "13.37.6",
"author": "Oflor",
"description": "Glaza zakryvaj",
"website_url": "https://git.vilunov.me/nikita/muzika-gromche",
"dependencies": [
"BepInEx-BepInExPack-5.4.2100"
]
}