mirror of
https://github.com/TheJordanDev/PEAK-JordanMod.git
synced 2026-10-16 23:56:44 +02:00
0.1.7 | Added: BetterBugle and ReplaceBingBong
This commit is contained in:
@@ -32,4 +32,9 @@
|
|||||||
# v0.1.6 | Stashed Bugle
|
# v0.1.6 | Stashed Bugle
|
||||||
|
|
||||||
- Added StashedBugleModule
|
- Added StashedBugleModule
|
||||||
- StashedBugleModule adds a new keybind to toggle give / remove a Bugle.
|
- StashedBugleModule adds a new keybind to toggle give / remove a Bugle.
|
||||||
|
|
||||||
|
# v0.1.7 | Better Bugle + Replace Bing Bong
|
||||||
|
|
||||||
|
- Added BetterBugle and ReplaceBingBong
|
||||||
|
- Play sounds and music in game with the Bugle (need a server to host the audio like mine), and can also with same server host sounds that start with SFX_VO_BingBong_ to replace voicelines (or add more) to BingBong
|
||||||
@@ -16,6 +16,16 @@ public static class ConfigHandler
|
|||||||
// Stashed Bugle settings
|
// Stashed Bugle settings
|
||||||
public static ConfigEntry<KeyCode> ToggleBugle { get; private set; } = null!;
|
public static ConfigEntry<KeyCode> ToggleBugle { get; private set; } = null!;
|
||||||
|
|
||||||
|
// Better Bugle settings
|
||||||
|
public static ConfigEntry<KeyCode> SyncAudioRepository { get; private set; } = null!;
|
||||||
|
public static ConfigEntry<float> BugleVolume { get; private set; } = null!;
|
||||||
|
public static ConfigEntry<string> BugleSoundAPIURL { get; private set; } = null!;
|
||||||
|
public static ConfigEntry<bool> AutoSyncAudioRepository { get; private set; } = null!;
|
||||||
|
public static ConfigEntry<string> AudioRepositorySubdirectory { get; private set; } = null!;
|
||||||
|
|
||||||
|
public static ConfigEntry<string> FavoriteSongsList { get; private set; } = null!;
|
||||||
|
public static ConfigEntry<KeyCode> FavoriteSongToggleKey { get; private set; } = null!;
|
||||||
|
|
||||||
public static void Initialize(ConfigFile configFile)
|
public static void Initialize(ConfigFile configFile)
|
||||||
{
|
{
|
||||||
Config = configFile;
|
Config = configFile;
|
||||||
@@ -46,6 +56,46 @@ public static class ConfigHandler
|
|||||||
KeyCode.V,
|
KeyCode.V,
|
||||||
new ConfigDescription("Give / destroy Bugle")
|
new ConfigDescription("Give / destroy Bugle")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Better Bugle settings
|
||||||
|
BugleVolume = Config.Bind(
|
||||||
|
"Better Bugle",
|
||||||
|
"BugleVolume",
|
||||||
|
0.5f,
|
||||||
|
new ConfigDescription(
|
||||||
|
"Bugle Sound Volume",
|
||||||
|
new AcceptableValueRange<float>(0f, 1f)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
BugleSoundAPIURL = Config.Bind(
|
||||||
|
"Better Bugle",
|
||||||
|
"BugleSoundAPIURL",
|
||||||
|
"",
|
||||||
|
new ConfigDescription("Bugle Sound API URL")
|
||||||
|
);
|
||||||
|
|
||||||
|
SyncAudioRepository = Config.Bind(
|
||||||
|
"Better Bugle",
|
||||||
|
"SyncAudioRepository",
|
||||||
|
KeyCode.L,
|
||||||
|
new ConfigDescription("Manually sync audio repository from git")
|
||||||
|
);
|
||||||
|
|
||||||
|
FavoriteSongsList = Config.Bind(
|
||||||
|
"Better Bugle",
|
||||||
|
"FavoriteSongsList",
|
||||||
|
"",
|
||||||
|
new ConfigDescription("Comma-separated list of favorite song names")
|
||||||
|
);
|
||||||
|
|
||||||
|
FavoriteSongToggleKey = Config.Bind(
|
||||||
|
"Better Bugle",
|
||||||
|
"FavoriteSongToggleKey",
|
||||||
|
KeyCode.Asterisk,
|
||||||
|
new ConfigDescription("Key to toggle favorite status of current song")
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<!-- This is the display name of your mod. Example: BepInEx Template -->
|
<!-- This is the display name of your mod. Example: BepInEx Template -->
|
||||||
<AssemblyTitle>JordanMod</AssemblyTitle>
|
<AssemblyTitle>JordanMod</AssemblyTitle>
|
||||||
<!-- This is the version number of your mod. -->
|
<!-- This is the version number of your mod. -->
|
||||||
<Version>0.1.6</Version>
|
<Version>0.1.7</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
577
src/JordanMod/modules/better_bugle/BetterBugleModule.cs
Normal file
577
src/JordanMod/modules/better_bugle/BetterBugleModule.cs
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Photon.Pun;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine.Audio;
|
||||||
|
using Zorro.Settings;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Zorro.Core;
|
||||||
|
using JordanMod.Utils;
|
||||||
|
|
||||||
|
namespace JordanMod.Modules.BetterBugle;
|
||||||
|
|
||||||
|
[Module(Enabled = true)]
|
||||||
|
class BetterBugleModule : Module
|
||||||
|
{
|
||||||
|
|
||||||
|
public static BetterBugleModule? Instance { get; private set; }
|
||||||
|
|
||||||
|
public override string ModuleName => "BetterBugle";
|
||||||
|
public static readonly string bugleItemName = "Bugle";
|
||||||
|
|
||||||
|
public static readonly string SoundsDirectory = Path.Combine(BepInEx.Paths.BepInExRootPath, "bugleSounds");
|
||||||
|
public static readonly Dictionary<string, AudioType> AudioTypes = new()
|
||||||
|
{
|
||||||
|
{ "wav", AudioType.WAV },
|
||||||
|
{ "mp3", AudioType.MPEG },
|
||||||
|
{ "ogg", AudioType.OGGVORBIS },
|
||||||
|
{ "aiff", AudioType.AIFF },
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsLoading { get; private set; } = false;
|
||||||
|
public static bool IsSyncing { get; private set; } = false;
|
||||||
|
public static int CurrentSongIndex { get; set; } = 0;
|
||||||
|
public static string CurrentSongName { get; set; } = "None";
|
||||||
|
public static bool HadConfirmation { get; set; } = false;
|
||||||
|
|
||||||
|
public static bool IsPlaying = false;
|
||||||
|
public static AudioSource? CurrentAudioSource { get; set; } = null;
|
||||||
|
|
||||||
|
public override Type[] GetPatches()
|
||||||
|
{
|
||||||
|
return [typeof(BetterBuglePatch)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
if (Instance != null) return;
|
||||||
|
Instance = this;
|
||||||
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||||
|
ManageLocalizedText();
|
||||||
|
GetAudioClips();
|
||||||
|
base.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
if (Input.GetKeyDown(ConfigHandler.SyncAudioRepository.Value))
|
||||||
|
{
|
||||||
|
Instance?.TrySyncAndLoadAudioClips();
|
||||||
|
}
|
||||||
|
if (Input.GetKeyDown(ConfigHandler.FavoriteSongToggleKey.Value))
|
||||||
|
{
|
||||||
|
if (Character.localCharacter == null) return;
|
||||||
|
if (Song.Songs.Count == 0) return;
|
||||||
|
if (!Song.Songs.ContainsKey(CurrentSongName)) return;
|
||||||
|
Character character = Character.localCharacter;
|
||||||
|
|
||||||
|
Optionable<byte> selectedSlot = character.refs.items.currentSelectedSlot;
|
||||||
|
if (selectedSlot.IsNone) return;
|
||||||
|
|
||||||
|
ItemSlot? itemSlot = character.player.itemSlots[selectedSlot.Value];
|
||||||
|
if (itemSlot == null) return;
|
||||||
|
|
||||||
|
Item? item = itemSlot.prefab;
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
List<string> supportedItemNames = ["Bugle", "Bugle_Magic", "Megaphone"];
|
||||||
|
if (!supportedItemNames.Contains(item.UIData.itemName)) return;
|
||||||
|
|
||||||
|
Song? currentSong = Song.Songs.GetValueOrDefault(CurrentSongName);
|
||||||
|
if (currentSong == null) return;
|
||||||
|
|
||||||
|
if (Song.FavoriteSongs.Contains(currentSong.Name))
|
||||||
|
{
|
||||||
|
Song.FavoriteSongs.Remove(currentSong.Name);
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($"Removed '{currentSong.Name}' from favorites.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Song.FavoriteSongs.Add(currentSong.Name);
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($"Added '{currentSong.Name}' to favorites.");
|
||||||
|
}
|
||||||
|
ConfigHandler.FavoriteSongsList.Value = string.Join("|-|", Song.FavoriteSongs);
|
||||||
|
|
||||||
|
}
|
||||||
|
base.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||||
|
{
|
||||||
|
if (!BetterBugleUI.Instance)
|
||||||
|
{
|
||||||
|
GameObject uiObject = new("BetterBugleUI");
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(uiObject);
|
||||||
|
uiObject.AddComponent<BetterBugleUI>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Destroy()
|
||||||
|
{
|
||||||
|
ClearAudioClips();
|
||||||
|
base.Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ManageLocalizedText()
|
||||||
|
{
|
||||||
|
List<string> secondaryActionLocalizations = new(LocalizedText.LANGUAGE_COUNT);
|
||||||
|
for (int i = 0; i < LocalizedText.LANGUAGE_COUNT; i++) secondaryActionLocalizations.Add("Refresh Songs");
|
||||||
|
|
||||||
|
List<string> scrollActionLocalizations = new(LocalizedText.LANGUAGE_COUNT);
|
||||||
|
for (int i = 0; i < LocalizedText.LANGUAGE_COUNT; i++) scrollActionLocalizations.Add("Change Song");
|
||||||
|
|
||||||
|
LocalizedText.mainTable.Add("SONG_LIST", secondaryActionLocalizations);
|
||||||
|
LocalizedText.mainTable.Add("CHANGE_SONG", scrollActionLocalizations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetAudioClips()
|
||||||
|
{
|
||||||
|
if (IsLoading || IsSyncing) return;
|
||||||
|
if (!Directory.Exists(SoundsDirectory)) return;
|
||||||
|
IsLoading = true;
|
||||||
|
Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory));
|
||||||
|
}
|
||||||
|
private void ClearAudioClips()
|
||||||
|
{
|
||||||
|
foreach (Song song in Song.Songs.Values.ToList())
|
||||||
|
{
|
||||||
|
song.Dispose();
|
||||||
|
}
|
||||||
|
Song.Sounds.Clear();
|
||||||
|
Song.SoundsByHash.Clear();
|
||||||
|
Song.Songs.Clear();
|
||||||
|
Song.BB_VoiceLines.Clear();
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
private IEnumerator LoadAllAudioClipsCoroutine(string directoryPath, string[]? forceReload = null)
|
||||||
|
{
|
||||||
|
List<(string filePath, string ext, string name)> filesToLoad = new();
|
||||||
|
|
||||||
|
foreach (var ext in AudioTypes.Keys)
|
||||||
|
{
|
||||||
|
var files = Directory.GetFiles(directoryPath, $"*.{ext}");
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
string name = Path.GetFileNameWithoutExtension(file);
|
||||||
|
bool shouldForceReload = forceReload != null && forceReload.Contains($"{name}.{ext}");
|
||||||
|
if (!Song.Songs.ContainsKey(name) || shouldForceReload)
|
||||||
|
{
|
||||||
|
filesToLoad.Add((file, ext, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int BATCH_SIZE = 2;
|
||||||
|
int loadedCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < filesToLoad.Count; i += BATCH_SIZE)
|
||||||
|
{
|
||||||
|
List<Coroutine> loadCoroutines = new();
|
||||||
|
|
||||||
|
for (int j = i; j < i + Math.Min(BATCH_SIZE, filesToLoad.Count - i) && j < filesToLoad.Count; j++)
|
||||||
|
{
|
||||||
|
var (filePath, ext, name) = filesToLoad[j];
|
||||||
|
bool forceReloadClip = forceReload != null && forceReload.Contains($"{name}.{ext}");
|
||||||
|
Coroutine loadCoroutine = Plugin.Instance.StartCoroutine(LoadAudioClipCoroutine(filePath, ext, name, forceReloadClip));
|
||||||
|
loadCoroutines.Add(loadCoroutine);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var coroutine in loadCoroutines) yield return coroutine;
|
||||||
|
loadedCount += loadCoroutines.Count;
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($"Loading audio clips... {loadedCount}/{filesToLoad.Count}");
|
||||||
|
}
|
||||||
|
OnAllAudioClipsLoaded();
|
||||||
|
}
|
||||||
|
private IEnumerator LoadAudioClipCoroutine(string filePath, string ext, string name, bool forceReload = false)
|
||||||
|
{
|
||||||
|
|
||||||
|
Debug.Log($"Loading audio clip: {name}.{ext} from {filePath}" + (forceReload ? " (forced reload)" : ""));
|
||||||
|
|
||||||
|
using UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip($"file://{filePath}", AudioTypes[ext]);
|
||||||
|
yield return www.SendWebRequest();
|
||||||
|
|
||||||
|
if (www.result != UnityWebRequest.Result.Success)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to load audio clip from {filePath}: {www.error}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool songExists = Song.Songs.ContainsKey(name);
|
||||||
|
|
||||||
|
Debug.Log($"Audio clip '{name}' exists: {songExists}. Force reload: {forceReload}");
|
||||||
|
|
||||||
|
if (songExists && !forceReload)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Audio clip with name '{name}' already exists. Skipping duplicate.");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songExists && forceReload)
|
||||||
|
{
|
||||||
|
Song? previousSong = Song.Songs.TryGetValue(name, out var existingSong) ? existingSong : null;
|
||||||
|
previousSong?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioClip audioClip = DownloadHandlerAudioClip.GetContent(www);
|
||||||
|
if (audioClip == null)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to load audio clip from {filePath}: {www.error}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Song song = new(name, ext, filePath, audioClip);
|
||||||
|
song.Register();
|
||||||
|
Debug.Log($"Loaded audio clip: {name} from {filePath}");
|
||||||
|
}
|
||||||
|
private void OnAllAudioClipsLoaded()
|
||||||
|
{
|
||||||
|
if (Song.Songs.Count == 0) Debug.LogWarning("No songs loaded. Please ensure audio files are in the Sounds directory.");
|
||||||
|
else Debug.Log($"🎵 {Song.Songs.Count} songs loaded !");
|
||||||
|
Song.UpdateRealIndices();
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($"{Song.Songs.Count} songs loaded !");
|
||||||
|
|
||||||
|
foreach (string songKeyName in ConfigHandler.FavoriteSongsList.Value.Split(["|-|"], StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
if (Song.Songs.ContainsKey(songKeyName) && !Song.FavoriteSongs.Contains(songKeyName))
|
||||||
|
Song.FavoriteSongs.Add(songKeyName);
|
||||||
|
|
||||||
|
if (!Song.Songs.ContainsKey(CurrentSongName))
|
||||||
|
CurrentSongName = Song.GetSongNames_Alphabetically()[CurrentSongIndex];
|
||||||
|
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TrySyncAndLoadAudioClips()
|
||||||
|
{
|
||||||
|
if (IsLoading || IsSyncing) return;
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
SyncAndLoadAudioClipsCoroutine().GetAwaiter().GetResult();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private async Task SyncAndLoadAudioClipsCoroutine()
|
||||||
|
{
|
||||||
|
if (IsLoading || IsSyncing) return;
|
||||||
|
IsSyncing = true;
|
||||||
|
AudioSyncService audioSyncService = AudioSyncService.GetInstance();
|
||||||
|
Dictionary<AudioSyncService.APIAudioFormat, Song?> toDownload = new();
|
||||||
|
|
||||||
|
string[] existingSongNames = Song.Songs.Keys.ToArray();
|
||||||
|
AudioSyncService.APIAudioFormat[] existingAPIFormats = [.. audioSyncService.GetAudioClips()];
|
||||||
|
string[] apiExistingNames = [.. existingAPIFormats.Select(apiAudio => apiAudio.Filename)];
|
||||||
|
|
||||||
|
var songsToRemove = existingSongNames.Except(apiExistingNames).ToArray();
|
||||||
|
foreach (var songName in songsToRemove)
|
||||||
|
{
|
||||||
|
if (Song.Songs.TryGetValue(songName, out var songToDispose))
|
||||||
|
{
|
||||||
|
songToDispose.Dispose();
|
||||||
|
songToDispose.DeleteFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach (AudioSyncService.APIAudioFormat apiAudio in existingAPIFormats)
|
||||||
|
{
|
||||||
|
Song? existingSong = Song.SoundsByHash.GetValueOrDefault(apiAudio.Hash);
|
||||||
|
if (existingSong == null || existingSong.Hash != apiAudio.Hash)
|
||||||
|
{
|
||||||
|
toDownload.Add(apiAudio, existingSong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($"Syncing audio bank... {toDownload.Count} changed/new files found.");
|
||||||
|
|
||||||
|
string[] filesToOverload = [];
|
||||||
|
|
||||||
|
foreach (AudioSyncService.APIAudioFormat apiAudio in toDownload.Keys)
|
||||||
|
{
|
||||||
|
bool success = await DownloadAPIAudio(apiAudio, toDownload[apiAudio]);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Debug.Log($"Successfully downloaded audio: {apiAudio.Filename}.{apiAudio.Extension}, adding to forceload");
|
||||||
|
filesToOverload = [.. filesToOverload, $"{apiAudio.Filename}.{apiAudio.Extension}"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IsSyncing = false;
|
||||||
|
IsLoading = true;
|
||||||
|
Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory, filesToOverload));
|
||||||
|
}
|
||||||
|
private async Task<bool> DownloadAPIAudio(AudioSyncService.APIAudioFormat apiAudio, Song? existingSong = null)
|
||||||
|
{
|
||||||
|
bool success = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (existingSong != null && apiAudio.Filename != existingSong.Name)
|
||||||
|
{
|
||||||
|
File.Delete(Path.Combine(SoundsDirectory, $"{existingSong.Name}.{existingSong.Extension}"));
|
||||||
|
}
|
||||||
|
await apiAudio.DownloadToFolder(SoundsDirectory);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to download API audio: {ex.Message}");
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BetterBugleSFX : MonoBehaviourPun
|
||||||
|
{
|
||||||
|
public Item? item;
|
||||||
|
public MagicBugle? magicBugle;
|
||||||
|
public bool isMegaphone = false;
|
||||||
|
public Song? song;
|
||||||
|
public AudioSource? audioSource;
|
||||||
|
public float GetVolume => (isMegaphone && item?._holderCharacter != Character.localCharacter) ? ConfigHandler.BugleVolume.Value * 2 : ConfigHandler.BugleVolume.Value;
|
||||||
|
|
||||||
|
public float maxBugleDistance = 500;
|
||||||
|
public float maxMegaphoneDistance = 1000;
|
||||||
|
|
||||||
|
public bool hold = false;
|
||||||
|
public bool isTooting = false;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
item = GetComponent<Item>();
|
||||||
|
TryGetComponent<MagicBugle>(out magicBugle);
|
||||||
|
audioSource = gameObject.AddComponent<AudioSource>();
|
||||||
|
audioSource.playOnAwake = false;
|
||||||
|
audioSource.maxDistance = isMegaphone ? maxMegaphoneDistance : maxBugleDistance;
|
||||||
|
audioSource.spatialBlend = 1f;
|
||||||
|
audioSource.volume = 0f;
|
||||||
|
audioSource.loop = true;
|
||||||
|
if (IsLocal()) BetterBugleModule.CurrentAudioSource = audioSource;
|
||||||
|
song = Song.Songs.GetValueOrDefault(BetterBugleModule.CurrentSongName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsLocal()
|
||||||
|
{
|
||||||
|
return item?._holderCharacter == Character.localCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (item == null || audioSource == null) return;
|
||||||
|
UpdateTooting();
|
||||||
|
if (hold && !isTooting)
|
||||||
|
{
|
||||||
|
audioSource.clip = song?.AudioClip;
|
||||||
|
if (audioSource.clip == null) return;
|
||||||
|
audioSource.Play();
|
||||||
|
audioSource.volume = GetVolume;
|
||||||
|
isTooting = true;
|
||||||
|
if (IsLocal()) BetterBugleModule.IsPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hold && isTooting)
|
||||||
|
{
|
||||||
|
isTooting = false;
|
||||||
|
if (IsLocal()) BetterBugleModule.IsPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hold) audioSource.volume = Mathf.Lerp(audioSource.volume, GetVolume, 10f * Time.deltaTime);
|
||||||
|
if (!hold) audioSource.volume = Mathf.Lerp(audioSource.volume, 0f, 10f * Time.deltaTime);
|
||||||
|
|
||||||
|
if (!isTooting && audioSource.volume <= 0.01f)
|
||||||
|
{
|
||||||
|
audioSource.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTooting()
|
||||||
|
{
|
||||||
|
if (item == null || audioSource == null) return;
|
||||||
|
if (!photonView.IsMine) return;
|
||||||
|
bool flag = item.isUsingPrimary;
|
||||||
|
if (magicBugle && magicBugle.currentFuel <= 0f) flag = false;
|
||||||
|
|
||||||
|
if (flag != hold)
|
||||||
|
{
|
||||||
|
if (flag) photonView.RPC("RPC_StartBetterToot", RpcTarget.All, BetterBugleModule.CurrentSongName);
|
||||||
|
else photonView.RPC("RPC_StopBetterToot", RpcTarget.All);
|
||||||
|
hold = flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[PunRPC]
|
||||||
|
private void RPC_StartBetterToot(string filename)
|
||||||
|
{
|
||||||
|
song = Song.Songs.GetValueOrDefault(filename);
|
||||||
|
if (song == null) return;
|
||||||
|
if (audioSource == null) return;
|
||||||
|
hold = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[PunRPC]
|
||||||
|
private void RPC_StopBetterToot()
|
||||||
|
{
|
||||||
|
if (audioSource == null) return;
|
||||||
|
hold = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BugleVolumeSettings : VolumeSetting
|
||||||
|
{
|
||||||
|
public BugleVolumeSettings(AudioMixerGroup mixerGroup) : base(mixerGroup)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetParameterName()
|
||||||
|
{
|
||||||
|
return "BugleVolume";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDisplayName()
|
||||||
|
{
|
||||||
|
return "Bugle Volume";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCategory()
|
||||||
|
{
|
||||||
|
return "Bugle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BetterBugleUI : MonoBehaviour
|
||||||
|
{
|
||||||
|
|
||||||
|
public static BetterBugleUI? Instance { get; private set; }
|
||||||
|
public bool IsVisible { get; private set; }
|
||||||
|
|
||||||
|
private float lastChangeTime = -10f;
|
||||||
|
|
||||||
|
private string soundDisplay = "";
|
||||||
|
|
||||||
|
private GUIStyle? customStyle;
|
||||||
|
|
||||||
|
private bool fontLoaded = false;
|
||||||
|
|
||||||
|
private int offsetX = 0;
|
||||||
|
|
||||||
|
private int offsetY = 70;
|
||||||
|
|
||||||
|
private int fontSize = 42;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null)
|
||||||
|
{
|
||||||
|
Debug.LogError("Multiple instances of BetterBugleUI detected!");
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
IsVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ShowActionbar(string message)
|
||||||
|
{
|
||||||
|
soundDisplay = message;
|
||||||
|
IsVisible = true;
|
||||||
|
lastChangeTime = Time.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
if (!fontLoaded)
|
||||||
|
{
|
||||||
|
Font[] array = Resources.FindObjectsOfTypeAll<Font>();
|
||||||
|
foreach (Font val in array)
|
||||||
|
{
|
||||||
|
if (val.name == "Tetsubin Gothic")
|
||||||
|
{
|
||||||
|
customStyle = new GUIStyle(GUI.skin.label);
|
||||||
|
customStyle.font = val;
|
||||||
|
customStyle.fontSize = fontSize;
|
||||||
|
customStyle.alignment = TextAnchor.LowerCenter;
|
||||||
|
customStyle.normal.textColor = Color.white;
|
||||||
|
fontLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (customStyle == null) return;
|
||||||
|
RenderSoundDisplay();
|
||||||
|
RenderPlayingDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderSoundDisplay()
|
||||||
|
{
|
||||||
|
if (customStyle == null) return;
|
||||||
|
if (!IsVisible) return;
|
||||||
|
if (Time.time - lastChangeTime > 3f)
|
||||||
|
{
|
||||||
|
IsVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxWidth = Screen.width - (offsetX * 2);
|
||||||
|
float textHeight = customStyle.CalcHeight(new GUIContent(soundDisplay), maxWidth);
|
||||||
|
|
||||||
|
// Align the bottom of the text block to (Screen.height - offsetY)
|
||||||
|
float y = Screen.height - offsetY - textHeight;
|
||||||
|
|
||||||
|
GUI.Label(new Rect(offsetX, y, maxWidth, textHeight), soundDisplay, customStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderPlayingDisplay()
|
||||||
|
{
|
||||||
|
if (customStyle == null) return;
|
||||||
|
if (BetterBugleModule.CurrentAudioSource == null || !BetterBugleModule.IsPlaying) return;
|
||||||
|
Song? currentAudio = Song.Songs.FirstOrDefault(s => s.Value.Name == BetterBugleModule.CurrentSongName).Value;
|
||||||
|
if (currentAudio == null || BetterBugleModule.CurrentAudioSource.clip == null) return;
|
||||||
|
|
||||||
|
float MAX_WIDTH = Screen.width - (offsetX * 2);
|
||||||
|
|
||||||
|
float audioLength = currentAudio.AudioClip.length;
|
||||||
|
float progress = BetterBugleModule.IsPlaying ? BetterBugleModule.CurrentAudioSource.time / audioLength : 0f;
|
||||||
|
|
||||||
|
// Progress bar: left to right, top right corner, with margin
|
||||||
|
float margin = 32f;
|
||||||
|
float barHeight = 18f;
|
||||||
|
float barWidth = Mathf.Min(400f, MAX_WIDTH * 0.5f); // reasonable max width
|
||||||
|
float barX = Screen.width - barWidth - margin;
|
||||||
|
float barY = margin;
|
||||||
|
|
||||||
|
// Draw background bar
|
||||||
|
Rect barRect = new Rect(barX, barY, barWidth, barHeight);
|
||||||
|
GUI.color = new Color(0f, 0f, 0f, 0.5f);
|
||||||
|
GUI.DrawTexture(barRect, Texture2D.whiteTexture, ScaleMode.StretchToFill, false);
|
||||||
|
|
||||||
|
// Draw progress circle (left to right)
|
||||||
|
float circleRadius = barHeight * 0.8f * 0.5f;
|
||||||
|
float circleCenterX = barX + barWidth * progress;
|
||||||
|
float circleCenterY = barY + barHeight / 2f;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
GUI.DrawTexture(new Rect(circleCenterX - circleRadius, circleCenterY - circleRadius, circleRadius * 2, circleRadius * 2), Texture2D.whiteTexture, ScaleMode.StretchToFill, true);
|
||||||
|
|
||||||
|
// Draw progress text under the bar, centered relative to the bar
|
||||||
|
float textY = barY + barHeight + 4f;
|
||||||
|
string FormatTime(float t)
|
||||||
|
{
|
||||||
|
int minutes = (int)t / 60;
|
||||||
|
float seconds = t % 60f;
|
||||||
|
return $"{minutes:00}:{seconds,5:00.00}";
|
||||||
|
}
|
||||||
|
string progressText = $"{FormatTime(BetterBugleModule.CurrentAudioSource.time)} - {FormatTime(audioLength)}";
|
||||||
|
GUIStyle textStyle = new GUIStyle(GUI.skin.label)
|
||||||
|
{
|
||||||
|
alignment = TextAnchor.UpperCenter,
|
||||||
|
fontSize = 16,
|
||||||
|
normal = { textColor = Color.white }
|
||||||
|
};
|
||||||
|
// Center the text horizontally relative to the bar
|
||||||
|
GUI.Label(new Rect(barX, textY, barWidth, 22f), progressText, textStyle);
|
||||||
|
|
||||||
|
// Reset color
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace JordanMod.Modules.ReplaceBingBong;
|
||||||
|
|
||||||
|
[Module(Enabled = true)]
|
||||||
|
class ReplaceBingBongModule : Module
|
||||||
|
{
|
||||||
|
public override string ModuleName => "Replace BingBong Module";
|
||||||
|
|
||||||
|
public override Type[] GetPatches()
|
||||||
|
{
|
||||||
|
return [typeof(ReplaceBingBongPatch)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
LocalizedText.mainTable.Add("idk_funny", ["Test subtitle!"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/JordanMod/patches/BetterBuglePatch.cs
Normal file
124
src/JordanMod/patches/BetterBuglePatch.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using HarmonyLib;
|
||||||
|
using JordanMod.Utils;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace JordanMod.Modules.BetterBugle;
|
||||||
|
|
||||||
|
public class BetterBuglePatch
|
||||||
|
{
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Item), "Start")]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
static void ItemStartPostfix(Item __instance)
|
||||||
|
{
|
||||||
|
if (__instance.itemState != ItemState.Held) return;
|
||||||
|
if (__instance.UIData == null) return;
|
||||||
|
|
||||||
|
List<string> supportedItemNames = ["Bugle", "Bugle_Magic", "Megaphone"];
|
||||||
|
|
||||||
|
if (!supportedItemNames.Contains(__instance.UIData.itemName)) return;
|
||||||
|
{
|
||||||
|
Action secondaryAction = OnRightClick;
|
||||||
|
Action<float> scrollAction = OnScroll;
|
||||||
|
|
||||||
|
__instance.UIData.hasSecondInteract = true;
|
||||||
|
__instance.UIData.hasScrollingInteract = true;
|
||||||
|
|
||||||
|
__instance.OnSecondaryStarted += secondaryAction;
|
||||||
|
__instance.OnScrolled += scrollAction;
|
||||||
|
|
||||||
|
__instance.UIData.secondaryInteractPrompt = "SONG_LIST";
|
||||||
|
__instance.UIData.scrollInteractPrompt = "CHANGE_SONG";
|
||||||
|
|
||||||
|
BetterBugleSFX betterBugleSFX = __instance.gameObject.AddComponent<BetterBugleSFX>();
|
||||||
|
if (__instance.UIData.itemName == "Megaphone") betterBugleSFX.isMegaphone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnRightClick()
|
||||||
|
{
|
||||||
|
// if (Song.Songs.Count == 0)
|
||||||
|
// {
|
||||||
|
// BetterBugleUI.Instance?.ShowActionbar("No songs available.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
if (BetterBugleModule.IsLoading) return;
|
||||||
|
if (!BetterBugleModule.HadConfirmation)
|
||||||
|
{
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar("Are you sure you want to refresh songs ? Right-click again to reload.");
|
||||||
|
BetterBugleModule.HadConfirmation = true;
|
||||||
|
Plugin.Instance.StartCoroutine(ResetConfirmation());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BetterBugleModule.HadConfirmation = false; // Reset confirmation state
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar("Refreshing songs...");
|
||||||
|
BetterBugleModule.Instance?.GetAudioClips();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator ResetConfirmation()
|
||||||
|
{
|
||||||
|
if (!BetterBugleModule.HadConfirmation) yield break;
|
||||||
|
yield return new WaitForSeconds(2f);
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar("No answer, not refreshing songs.");
|
||||||
|
BetterBugleModule.HadConfirmation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnScroll(float scrollDelta)
|
||||||
|
{
|
||||||
|
if (BetterBugleModule.IsLoading) return;
|
||||||
|
bool isNext = scrollDelta > 0;
|
||||||
|
if (Song.Songs.Count == 0)
|
||||||
|
{
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar("No songs available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNext && BetterBugleModule.CurrentSongIndex < Song.Songs.Count - 1) BetterBugleModule.CurrentSongIndex++;
|
||||||
|
else if (isNext && BetterBugleModule.CurrentSongIndex == Song.Songs.Count - 1) BetterBugleModule.CurrentSongIndex = 0;
|
||||||
|
else if (!isNext && BetterBugleModule.CurrentSongIndex > 0) BetterBugleModule.CurrentSongIndex--;
|
||||||
|
else BetterBugleModule.CurrentSongIndex = Song.Songs.Count - 1;
|
||||||
|
BetterBugleModule.CurrentSongName = Song.GetSongNames_Alphabetically()[BetterBugleModule.CurrentSongIndex];
|
||||||
|
|
||||||
|
Song currentSong = Song.Songs[BetterBugleModule.CurrentSongName];
|
||||||
|
|
||||||
|
bool isFavorite = Song.FavoriteSongs.Contains(BetterBugleModule.CurrentSongName);
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($" {(isFavorite ? "★" : " ")} {currentSong.RealIndex} | {currentSong.Name.Replace("_", " ")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(CharacterItems), "Awake")]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
static void CharacterItemsEquipPostfix(CharacterItems __instance)
|
||||||
|
{
|
||||||
|
__instance.onSlotEquipped += () =>
|
||||||
|
{
|
||||||
|
if (__instance.character == null || __instance.character != Character.localCharacter) return;
|
||||||
|
Item? currentItem = __instance.character.data.currentItem;
|
||||||
|
if (currentItem == null || currentItem.UIData == null) return;
|
||||||
|
if (currentItem.itemState != ItemState.Held) return;
|
||||||
|
if (currentItem.TryGetComponent<BugleSFX>(out var bugleSFX))
|
||||||
|
{
|
||||||
|
Song? song = Song.Songs.GetValueOrDefault(BetterBugleModule.CurrentSongName);
|
||||||
|
if (song == null) return;
|
||||||
|
|
||||||
|
bool isFavorite = Song.FavoriteSongs.Contains(BetterBugleModule.CurrentSongName);
|
||||||
|
BetterBugleUI.Instance?.ShowActionbar($"{(isFavorite ? "★" : " ")} {song.RealIndex} | {song.Name}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(BugleSFX), "Update")]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
static void BugleSFXUpdatePostfix(BugleSFX __instance)
|
||||||
|
{
|
||||||
|
if (__instance.volume > 0f) __instance.volume = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
70
src/JordanMod/patches/ReplaceBingBongPatch.cs
Normal file
70
src/JordanMod/patches/ReplaceBingBongPatch.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using HarmonyLib;
|
||||||
|
using JordanMod.Utils;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace JordanMod.Modules.ReplaceBingBong;
|
||||||
|
|
||||||
|
public class ReplaceBingBongPatch
|
||||||
|
{
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(ItemActionBase), "OnEnable")]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
static bool PreActionAskBingBongConstructorFix(ItemActionBase __instance)
|
||||||
|
{
|
||||||
|
if (__instance is not Action_AskBingBong askBingBong)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Action_AskBingBong.BingBongResponse[] currentResponses = [..askBingBong.responses];
|
||||||
|
// Each response has a .sfx which has a Object.name, store a ref to the sfx with key being sfx name
|
||||||
|
Dictionary<string, SFX_Instance> sfxDict = new();
|
||||||
|
for (int i = 0; i < currentResponses.Length; i++)
|
||||||
|
{
|
||||||
|
Action_AskBingBong.BingBongResponse response = currentResponses[i];
|
||||||
|
if (response.sfx != null && response.sfx.clips != null && response.sfx.clips.Length > 0)
|
||||||
|
{
|
||||||
|
foreach (AudioClip clip in response.sfx.clips)
|
||||||
|
{
|
||||||
|
sfxDict[response.sfx.name] = response.sfx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Song> voices = [.. Song.BB_VoiceLines.Values];
|
||||||
|
|
||||||
|
foreach (Song voice in voices)
|
||||||
|
{
|
||||||
|
AudioClip clip = voice.AudioClip;
|
||||||
|
|
||||||
|
bool isNew = !sfxDict.ContainsKey(voice.Name);
|
||||||
|
if (isNew)
|
||||||
|
{
|
||||||
|
SFX_Instance sFX_Instance = new()
|
||||||
|
{
|
||||||
|
clips = [clip]
|
||||||
|
};
|
||||||
|
Action_AskBingBong.BingBongResponse newResponse = new()
|
||||||
|
{
|
||||||
|
sfx = sFX_Instance,
|
||||||
|
subtitleID = "idk_funny",
|
||||||
|
mouthCurve = null,
|
||||||
|
mouthCurveTime = 1f
|
||||||
|
};
|
||||||
|
currentResponses = [.. currentResponses, newResponse];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sfxDict[voice.Name].clips = [clip];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
askBingBong.responses = new Action_AskBingBong.BingBongResponse[currentResponses.Length];
|
||||||
|
for (int i = 0; i < currentResponses.Length; i++)
|
||||||
|
{
|
||||||
|
askBingBong.responses[i] = currentResponses[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
198
src/JordanMod/utils/AudioSyncService.cs
Normal file
198
src/JordanMod/utils/AudioSyncService.cs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JordanMod.Modules.BetterBugle;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace JordanMod.Utils;
|
||||||
|
|
||||||
|
class AudioSyncService
|
||||||
|
{
|
||||||
|
public static string API_BASE_URL => ConfigHandler.BugleSoundAPIURL.Value;
|
||||||
|
|
||||||
|
private static AudioSyncService? Instance { get; set; }
|
||||||
|
public static AudioSyncService GetInstance()
|
||||||
|
{
|
||||||
|
Instance ??= new AudioSyncService();
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<APIAudioFormat> GetAudioClips()
|
||||||
|
{
|
||||||
|
List<APIAudioFormat> audioClips = [];
|
||||||
|
|
||||||
|
Uri? uri = Uri.TryCreate($"{API_BASE_URL}/audio/list", UriKind.Absolute, out var result) ? result : null;
|
||||||
|
if (uri == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("Invalid BugleSoundAPIURL in config.");
|
||||||
|
return audioClips;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var client = new System.Net.WebClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = client.DownloadString(uri);
|
||||||
|
var data = JsonConvert.DeserializeObject<List<APIAudioFormat>>(json);
|
||||||
|
if (data == null) return audioClips;
|
||||||
|
audioClips.AddRange(data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to fetch or parse audio clip hashes: {ex.Message}");
|
||||||
|
}
|
||||||
|
return audioClips;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class APIAudioFormat
|
||||||
|
{
|
||||||
|
[JsonProperty("_id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("filename")]
|
||||||
|
public string Filename { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("extension")]
|
||||||
|
public string Extension { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("size")]
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("hash")]
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("modified_at")]
|
||||||
|
public DateTime ModifiedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("owner")]
|
||||||
|
public string Owner { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public async Task DownloadToFolder(string folderPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Filename) || string.IsNullOrEmpty(Extension))
|
||||||
|
{
|
||||||
|
Debug.LogError("Invalid audio file information.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string filePath = Path.Combine(folderPath, $"{Filename}.{Extension}");
|
||||||
|
|
||||||
|
// Ensure the directory exists
|
||||||
|
Directory.CreateDirectory(folderPath);
|
||||||
|
|
||||||
|
if (File.Exists(filePath)) File.Delete(filePath);
|
||||||
|
|
||||||
|
string url = $"{API_BASE_URL}/audio/{Id}/download?hash={Hash}";
|
||||||
|
Debug.LogError($"Downloading audio from URL: {url}");
|
||||||
|
|
||||||
|
using UnityWebRequest www = UnityWebRequest.Get(url);
|
||||||
|
var operation = www.SendWebRequest();
|
||||||
|
while (!operation.isDone) await Task.Yield();
|
||||||
|
if (www.result != UnityWebRequest.Result.Success)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to download API audio: {www.error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File.WriteAllBytes(filePath, www.downloadHandler.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Song : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly Dictionary<string, Song> Sounds = new();
|
||||||
|
public static readonly Dictionary<string, Song> SoundsByHash = new();
|
||||||
|
|
||||||
|
public static readonly Dictionary<string, Song> Songs = new();
|
||||||
|
public static readonly Dictionary<string, Song> BB_VoiceLines = new();
|
||||||
|
|
||||||
|
public static List<string> FavoriteSongs = new();
|
||||||
|
|
||||||
|
public static List<string> GetSongNames_Alphabetically()
|
||||||
|
{
|
||||||
|
return [.. new List<string>(Songs.Keys)
|
||||||
|
.OrderByDescending(FavoriteSongs.Contains)
|
||||||
|
.ThenBy(name => name)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateRealIndices()
|
||||||
|
{
|
||||||
|
var sortedNames = Songs.Keys.OrderBy(name => name).ToList();
|
||||||
|
for (int i = 0; i < sortedNames.Count; i++)
|
||||||
|
{
|
||||||
|
if (Songs.TryGetValue(sortedNames[i], out var song))
|
||||||
|
{
|
||||||
|
song.RealIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Extension { get; set; }
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
public AudioClip AudioClip { get; }
|
||||||
|
public string Hash { get; }
|
||||||
|
public int RealIndex { get; set; }
|
||||||
|
|
||||||
|
public Song(string name, string extension, string filePath, AudioClip audioClip)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Extension = extension;
|
||||||
|
FilePath = filePath;
|
||||||
|
AudioClip = audioClip;
|
||||||
|
Hash = GenerateHash(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register()
|
||||||
|
{
|
||||||
|
Sounds[Name] = this;
|
||||||
|
SoundsByHash[Hash] = this;
|
||||||
|
if (Name.StartsWith("SFX_VO_BingBong_")) BB_VoiceLines[Name] = this;
|
||||||
|
else Songs[Name] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (AudioClip == null) return;
|
||||||
|
var audioSources = UnityEngine.Object.FindObjectsByType<AudioSource>(FindObjectsSortMode.None);
|
||||||
|
foreach (var audioSource in audioSources)
|
||||||
|
{
|
||||||
|
if (audioSource.clip == AudioClip)
|
||||||
|
{
|
||||||
|
audioSource.Stop();
|
||||||
|
audioSource.clip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sounds.Remove(Name);
|
||||||
|
SoundsByHash.Remove(Hash);
|
||||||
|
if (Name.StartsWith("SFX_VO_BingBong_")) BB_VoiceLines.Remove(Name);
|
||||||
|
else Songs.Remove(Name);
|
||||||
|
UnityEngine.Object.Destroy(AudioClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile()
|
||||||
|
{
|
||||||
|
if (AudioClip == null) return;
|
||||||
|
var filePath = Path.Combine(BetterBugleModule.SoundsDirectory, $"{Name}.{Extension}");
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
Debug.Log($"Deleted local file: {filePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateHash(string filePath)
|
||||||
|
{
|
||||||
|
using var hasher = SHA256.Create();
|
||||||
|
var fileBytes = File.ReadAllBytes(filePath);
|
||||||
|
var hashBytes = hasher.ComputeHash(fileBytes);
|
||||||
|
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user