727 lines
26 KiB
C#

using MelonLoader;
using UnityEngine;
using FMODUnity;
using FMOD.Studio;
using HarmonyLib;
using Rhythm;
using static Rhythm.BeatmapIndex;
using Arcade.UI;
using System.Runtime.InteropServices;
using FMOD;
using static UnbeatableSongHack.TreeExplorer;
using Arcade.UI.SongSelect;
using static Arcade.UI.SongSelect.ArcadeSongDatabase;
using UnityEngine.UIElements;
using System.Xml.Linq;
using static Dreamteck.Splines.SplineSampleModifier;
[assembly: MelonInfo(typeof(UnbeatableSongHack.Core), "SUnbeatable-Loader", "1.0.0", "Serena Sunskimmer82", null)]
[assembly: MelonGame("D-CELL GAMES", "UNBEATABLE [DEMO]")]
namespace UnbeatableSongHack
{
public class Core : MelonMod
{
public override void OnInitializeMelon()
{
LoggerInstance.Msg("Initialized Song Hack!");
}
public override void OnLateUpdate()
{
if (Input.GetKeyDown(KeyCode.N))
{
// Stop all events
RuntimeManager.GetBus("bus:/").stopAllEvents(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
}
if (Input.GetKeyDown(KeyCode.M))
{
//FetchAllEvents();
}
}
public List<FMOD.Studio.EventDescription> eventDescriptions = new List<EventDescription>();
public List<string> strings = new List<string>();
// Get the events from the bank
// These can be used to play single audios in game through the tree menu
FMOD.Studio.EventDescription[] GetBankEvents(Bank bank)
{
LoggerInstance.Msg("\n> Getting Bank Events");
bank.getEventList(out FMOD.Studio.EventDescription[] eventDescs);
foreach (FMOD.Studio.EventDescription eventDesc in eventDescs)
{
eventDesc.getPath(out string eventPath);
LoggerInstance.Msg(eventPath);
eventDescriptions.Add(eventDesc);
}
return eventDescs;
}
// Read all string events from the bank
void GetBankStrings(Bank bank)
{
LoggerInstance.Msg("\n> Getting Bank Strings");
bank.getStringCount(out int stringCount);
LoggerInstance.Msg("String Count: " + stringCount);
for (int i = 0; i < stringCount; i++)
{
bank.getStringInfo(i, out FMOD.GUID Id, out string stringPath);
LoggerInstance.Msg("String ID: " + Id);
LoggerInstance.Msg("String Path: " + stringPath);
LoggerInstance.Msg("---");
// Strings zur Liste hinzufügen
if (!string.IsNullOrEmpty(stringPath))
{
strings.Add(stringPath);
}
}
}
// Build the event tree from the event descriptions and strings
void FetchAllEvents()
{
// Listen leeren
eventDescriptions.Clear();
strings.Clear();
rootNode = new TreeNode("Root");
RuntimeManager.StudioSystem.getBankList(out Bank[] banks);
foreach (Bank bank in banks)
{
// Get the name of the bank
bank.getPath(out string bankPath);
// Log the name of the bank
LoggerInstance.Msg("--- Bank Path: " + bankPath);
// It's Master.strings! Read the strings from it!
if (bankPath.EndsWith("strings"))
{
GetBankStrings(bank);
}
try
{
GetBankEvents(bank);
}
catch (System.Exception e)
{
LoggerInstance.Msg("Error fetching events: " + e.Message);
}
}
BuildEventTree(out rootNode, eventDescriptions, strings);
}
// "Progessions" are some kind of class
public class CustomProgression: IProgression
{
public string stageScene = "TrainStationRhythm";
public string filePath = "./none.txt";
public string audioKey = "EMPTY DIARY";
public CustomProgression(string textFile)
{
this.filePath = textFile;
// Look for an audio.mp3 in the same directory as the beatmap
// Might as well use the audioFilename property from the beatmap itself later
string directory = Path.GetDirectoryName(filePath);
var audioPath = Path.Combine(directory, "audio.mp3");
// If an audio is found, set it to the audio path
var audioTitle = "EMPTY DIARY";
if (File.Exists(audioPath))
{
audioTitle = audioPath;
}
this.audioKey = audioTitle;
this.stageScene = "TrainStationRhythm";
}
public string GetBeatmapPath()
{
// Placeholder
return "__CUSTOM/CUSTOMDIFF";
}
public string GetSongName()
{
return audioKey;
}
public string GetDifficulty()
{
// Placeholder
return "CUSTOMDIFF";
}
public void Finish(string sceneIndex)
{
LevelManager.LoadLevel("ScoreScreenArcadeMode");
}
public void Retry()
{
LevelManager.LoadLevel(stageScene);
}
public void Back()
{
LevelManager.LoadLevel(JeffBezosController.arcadeMenuScene);
}
}
void LoadBeatmapFromFile(string filePath="")
{
if (filePath.Length==0) return;
CustomProgression customProgression = new CustomProgression(filePath);
JeffBezosController.rhythmProgression = customProgression;
LevelManager.LoadLevel(customProgression.stageScene);
}
// Detect if the beatmap is a custom one
// Since ParseBeatmap attempts to load a TextAsset (which we cant create)
// we have to patch the function and make it load from text contents instead.
[HarmonyPatch(typeof(BeatmapParser), "ParseBeatmap", new Type[] {})]
public class BeatmapParserPatch
{
public static bool Prefix(ref BeatmapParser __instance)
{
if (JeffBezosController.rhythmProgression.GetBeatmapPath().StartsWith("__CUSTOM"))
{
var progression = JeffBezosController.rhythmProgression as CustomProgression;
__instance.beatmapIndex = BeatmapIndex.defaultIndex;
var contents = File.ReadAllText(progression.filePath);
BeatmapParserEngine beatmapParserEngine = new BeatmapParserEngine();
var beatmap = ScriptableObject.CreateInstance<Beatmap>();
beatmapParserEngine.ReadBeatmap(contents, ref beatmap);
__instance.beatmap = beatmap;
__instance.audioKey = progression.GetSongName();
__instance.beatmapPath = progression.GetBeatmapPath();
return false;
}
return true;
}
}
// Patch to load beatmaps from file
// Since the game really wants to load from its own audio table,
// we need to patch this to load from a file instead
// That way we can make the game load any sound file (and in this case,
// the one from our custom level)
[HarmonyPatch(typeof(RhythmTracker), "PreloadFromTable")]
public class RhythmTrackerLoadPatch
{
public static bool Prefix(string key, ref RhythmTracker __instance)
{
if (key.Contains("."))
{
if (File.Exists(key))
{
__instance.PreloadFromFile(key);
return false;
} else
{
return true;
}
}
return true;
}
}
void PlayFromKey(string name)
{
var arcadeBGMManger = ArcadeBGMManager.Instance;
var songList = ArcadeSongDatabase.Instance;
var beatmapIndex = BeatmapIndex.defaultIndex;
if (arcadeBGMManger != null && songList != null)
{
LoggerInstance.Msg("Adding key: " + name);
var beatmapItem = new BeatmapItem();
beatmapItem.Path = "__CUSTOM" + name + "/Beginner";
var key = beatmapItem.Path;
beatmapItem.Song = new Song(name);
beatmapItem.Song.stageScene = "TrainStationRhythm";
beatmapItem.Unlocked = true;
beatmapItem.BeatmapInfo = new BeatmapInfo(null, "Beginner");
beatmapItem.Highscore = new HighScoreItem(key, 0, 0f, 0, cleared: false, new Dictionary<string, int>(), Modifiers.None);
LoggerInstance.Msg("Adding song to list: " + beatmapItem.Path);
// This whole part would be needed for AddSongList()
beatmapItem.Beatmap = ScriptableObject.CreateInstance<Beatmap>();
beatmapItem.Beatmap.metadata.title = "Custom " + name;
beatmapItem.Beatmap.metadata.titleUnicode = "Custom " + name;
beatmapItem.Beatmap.metadata.artist = "Not You";
beatmapItem.Beatmap.metadata.artistUnicode = "Not You";
beatmapItem.Beatmap.metadata.tagData.Level = 10;
// This was a test and is not needed
//AddSongToArcadeList(songList,beatmapItem);
LoggerInstance.Msg("Added Key: " + key);
arcadeBGMManger.PlaySongPreview(beatmapItem);
}
}
public static string lastEventPath = "";
public TreeNode rootNode = new TreeNode("Root");
public Dictionary<string, bool> expandedStates = new Dictionary<string, bool>();
public Rect windowRect = new Rect(20, 20, 400, 600);
public Vector2 scrollPosition = new Vector2(0, 0);
public string textInput = "";
public override void OnGUI()
{
windowRect = GUI.Window(0, windowRect, DoWindow, "Song Hack");
if (windowRect.width == 0)
{
if (GUI.Button(new Rect(10, 10, 32, 32), "O"))
{
windowRect = new Rect(20, 20, 400, 600);
}
}
}
// Draw MOD UI
void DoWindow(int windowId)
{
GUIStyle compactButtonStyle = new GUIStyle(GUI.skin.button);
compactButtonStyle.alignment = TextAnchor.MiddleLeft;
compactButtonStyle.fontSize = 14;
GUIStyle compactEnabledButtonStyle = new GUIStyle(compactButtonStyle);
compactEnabledButtonStyle.normal.textColor = Color.green;
GUIStyle folderStyle = new GUIStyle(GUI.skin.button);
folderStyle.alignment = TextAnchor.MiddleLeft;
folderStyle.fontSize = 14;
folderStyle.normal.textColor = Color.yellow;
GUIStyle leafStyle = new GUIStyle(GUI.skin.button);
leafStyle.alignment = TextAnchor.MiddleLeft;
leafStyle.fontSize = 14;
leafStyle.normal.textColor = Color.white;
GUIStyle stringStyle = new GUIStyle(GUI.skin.button);
stringStyle.alignment = TextAnchor.MiddleLeft;
stringStyle.fontSize = 14;
stringStyle.normal.textColor = Color.cyan;
GUIStyle playableStringStyle = new GUIStyle(GUI.skin.button);
playableStringStyle.alignment = TextAnchor.MiddleLeft;
playableStringStyle.fontSize = 14;
playableStringStyle.normal.textColor = Color.green;
GUI.DragWindow(new Rect(0, 0, 10000, 20));
// Create button to stop all events
if (GUI.Button(new Rect(10, 25, 110, 30), "Stop All Events"))
{
RuntimeManager.GetBus("bus:/").stopAllEvents(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
}
// Create button to get all events
if (GUI.Button(new Rect(120, 25, 110, 30), "Get All Events"))
{
FetchAllEvents();
LoggerInstance.Msg($"Fetched {eventDescriptions.Count} events and {strings.Count} strings.");
}
// Play an event from the text input
if (GUI.Button(new Rect(230, 25, 110, 30), "Sound from Text"))
{
if (!string.IsNullOrEmpty(textInput))
{
RuntimeManager.PlayOneShot(textInput);
lastEventPath = textInput;
LoggerInstance.Msg("Playing: " + textInput);
}
}
if (GUI.Button(new Rect(230, 55, 110, 25), "Sound from Key"))
{
if (!string.IsNullOrEmpty(textInput))
{
PlayFromKey(textInput);
lastEventPath = "Key: " + textInput;
LoggerInstance.Msg("Playing Key: " + textInput);
}
}
if (GUI.Button(new Rect(130, 55, 100, 25), "Play File Level"))
{
if (!string.IsNullOrEmpty(textInput))
{
LoadBeatmapFromFile(textInput);
} else
{
LoadBeatmapFromFile();
}
}
if (GUI.Button(new Rect(10,120,100,25), "Add Custom Songs To Arcade"))
{
var db = Arcade.UI.SongSelect.ArcadeSongDatabase.Instance;
string customSongDir = $"{Application.dataPath.Substring(0, Application.dataPath.LastIndexOf('/'))}/SUnbeatable-Loader/Songs/";
if (!Directory.Exists(customSongDir)) Directory.CreateDirectory(customSongDir);
LoggerInstance.Msg($"SUnbeatable-Loader Song Directory: {customSongDir}");
string[] dirs = Directory.GetDirectories(customSongDir);
foreach (var dir in dirs)
{
string songDir = Path.GetFileNameWithoutExtension(dir);
LoggerInstance.Msg($"{songDir}");
//new demo maps are by default stored as .dat files
string[] songFiles = Directory.GetFiles(customSongDir + songDir, "*.dat");
foreach (var songFile in songFiles)
{
string beatmapName = Path.GetFileNameWithoutExtension(songFile);
string beatmapContent = File.ReadAllText(songFile);
string difficulty = beatmapContent.Substring(beatmapContent.IndexOf("Version:")+8 /* adding 8, the length of the string "Version:" */ , beatmapContent.IndexOf(" ", beatmapContent.IndexOf("Version:"))-(beatmapContent.IndexOf("Version:")+8));
LoggerInstance.Msg($"{beatmapName} has difficulty {difficulty}");
BeatmapItem item = new BeatmapItem();
item.Path = "__CUSTOM" + songFile;
item.Song = new Song(beatmapName);
TextAsset asset = new TextAsset(beatmapContent);
asset.name = beatmapName;
item.BeatmapInfo = new BeatmapInfo(asset,difficulty);
item.Beatmap = ScriptableObject.CreateInstance<Beatmap>();
item.Beatmap.name = beatmapName;
item.Beatmap.metadata.title = beatmapName;
item.Beatmap.metadata.titleUnicode = beatmapName;
item.Beatmap.metadata.artist = "ARTIST";
item.Beatmap.metadata.artistUnicode = "ARTIST";
item.Beatmap.metadata.tagData.Level = 10;
item.Beatmap.metadata.tagData.SongLength = 100;
AddSongToArcadeList(db, item);
LoggerInstance.Msg("adding beatmap " + beatmapName + " at " + songDir);
}
}
}
if (GUI.Button(new Rect(110, 120, 100, 25), "Add To Arcade"))
{
if (!string.IsNullOrEmpty(textInput))
{
CustomProgression progression = new CustomProgression(textInput);
var beatmapItem = new BeatmapItem();
beatmapItem.Path = "__CUSTOM" + progression.GetSongName() + "/Beginner";
var key = beatmapItem.Path;
beatmapItem.Song = /*new Song(progression.GetSongName().Substring(progression.GetSongName().LastIndexOf("/")+1,progression.GetSongName().LastIndexOf(".")))*/null;
beatmapItem.Song.stageScene = "TrainStationRhythm";
beatmapItem.Unlocked = true;
beatmapItem.BeatmapInfo = new BeatmapInfo(null, "Beginner");
beatmapItem.Highscore = new HighScoreItem(key, 0, 0f, 0, cleared: false, new Dictionary<string, int>(), Modifiers.None);
LoggerInstance.Msg("Adding song to list: " + beatmapItem.Path);
beatmapItem.Beatmap = new Beatmap();
beatmapItem.Beatmap.metadata.title = "Custom " + progression.GetSongName();
beatmapItem.Beatmap.metadata.titleUnicode = "Custom " + progression.GetSongName();
beatmapItem.Beatmap.metadata.artist = "Not You";
beatmapItem.Beatmap.metadata.artistUnicode = "Not You";
beatmapItem.Beatmap.metadata.tagData.Level = 10;
var db = Arcade.UI.SongSelect.ArcadeSongDatabase.Instance;
AddSongToArcadeList(db, beatmapItem);
}
}
if (GUI.Button(new Rect(340, 25, 50, 30), "X"))
{
windowRect = new Rect(-100, -100, 0, 0);
}
textInput = GUI.TextField(new Rect(10, 80, 360, 25), textInput);
int totalHeight = CalculateTreeHeight(rootNode);
scrollPosition = GUI.BeginScrollView(
new Rect(0, 180, 400, 440),
scrollPosition,
new Rect(0, 0, 380, totalHeight)
);
int yPos = 0;
DrawTreeNode(rootNode, 0, ref yPos, folderStyle, leafStyle, stringStyle, playableStringStyle);
GUI.EndScrollView();
}
// Let other classes access the logger
public static MelonLogger.Instance GetLogger()
{
MelonBase core = Core.FindMelon("SUnbeatable-Loader", "Serena Sunskimmer82");
return core.LoggerInstance;
}
// Patch the song function to return all (also hidden) songs,
// so we can access hidden beatmaps
[HarmonyPatch(typeof(BeatmapIndex), "GetVisibleSongs")]
public class BeatmapIndexPatch
{
public static bool Prefix(ref BeatmapIndex __instance, ref List<Song> __result)
{
__result = __instance.GetAllSongs();
return false;
}
}
[HarmonyPatch(typeof(RhythmTracker), "HandleCreateProgrammerSound", new Type[] { typeof(EventInstance), typeof(IntPtr) })]
public static class RhythmTrackerPatch
{
public static bool Prefix(EventInstance instance, IntPtr parameterPtr)
{
var LoggerInstance = Core.GetLogger();
//LoggerInstance.Msg("HandleCreateProgrammerSound called");
instance.getUserData(out var h_userdata);
PlayInfo h_playInfo = GCHandle.FromIntPtr(h_userdata).Target as PlayInfo;
if (h_playInfo.source == PlaySource.FromTable)
{
string key = Marshal.PtrToStringUni(h_playInfo.key);
// Log the key of the sound being played
// Useful since we can't find the sound name otherwise
// Was used to match the names to the unnamed soundtrack files
LoggerInstance.Msg("Now playing: " + key);
//SOUND_INFO h_info;
//RESULT h_soundInfo = RuntimeManager.StudioSystem.getSoundInfo(key, out h_info);
//LoggerInstance.Msg("SoundInfo: " + (h_info.name));
}
//instance.getVolume(out float volume);
//LoggerInstance.Msg("Volume: " + volume.ToString());
return true;
}
}
// Doesn't really work, but it sometimes logs when something is played
[HarmonyPatch(typeof(FMODUnity.RuntimeManager), "PlayOneShot", new Type[] { typeof(string), typeof(Vector3) })]
public class RuntimeManagerPatch
{
public static bool Prefix(string path)
{
var LoggerInstance = Core.GetLogger();
LoggerInstance.Msg("[Patch] Playing: " + path);
return true;
}
}
// Function to add arbitrary beatmap songs into the arcade list
// Might be useful for some custom beatmaps, although some info is fetched from
// TextAssets that we cannot create/access through mods.
// It may be a mess.
public static void AddSongToArcadeList(ArcadeSongDatabase instance, ArcadeSongDatabase.BeatmapItem item)
{
var LoggerInstance = Core.GetLogger();
string key = item.Path;
item.Unlocked = true;
Traverse traverse = Traverse.Create(instance);
Dictionary<string, BeatmapItem> songList = traverse.Field("_songDatabase").GetValue<Dictionary<string, BeatmapItem>>();
LoggerInstance.Msg("trying to add " + key);
var success = songList.TryAdd(key, item);
LoggerInstance.Msg(success);
traverse.Field("_songDatabase").SetValue(songList);
instance.RefreshSongList();
LoggerInstance.Msg("Adding song to list: " + item.Path);
}
[HarmonyPatch(typeof(ArcadeSongDatabase), "Awake")]
public class ArcadeSongDatabasePatch
{
public static void Postfix(ref ArcadeSongDatabase __instance)
{
var LoggerInstance = Core.GetLogger();
Traverse traverse = Traverse.Create(__instance);
BeatmapIndex beatmapIndex = traverse.Field("_beatmapIndex").GetValue<BeatmapIndex>();
Dictionary<string, BeatmapItem> songDatabase = traverse.Field("_songDatabase").GetValue<Dictionary<string, BeatmapItem>>();
List<BeatmapItem> songList = traverse.Field("_songList").GetValue<List<BeatmapItem>>();
string beatmapPath = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\UNBEATABLE Demo\\SUnbeatable-Loader\\Songs\\Empty Diary (Custom)\\beatmap.dat";
//test song - Empty Diaryaaa
string beatmap = File.ReadAllText(beatmapPath);
string songName = "audio.mp3";
using (var songNameReader = new StringReader(beatmap.Substring(beatmap.IndexOf("Title:") + 6).Trim()))
{
//songName = songNameReader.ReadLine();
}
LoggerInstance.Msg(songName);
string difficulty = beatmap.Substring(beatmap.IndexOf("Version:") + 8 /* adding 8, the length of the string "Version:" */ , beatmap.IndexOf(" ", beatmap.IndexOf("Version:")) - (beatmap.IndexOf("Version:") + 8));
LoggerInstance.Msg(difficulty);
string path = songName+"/"+difficulty;
BeatmapIndex.Song mapSong = new Song(songName);
mapSong.stageScene = "TrainStationRhythm";
TextAsset asset = new TextAsset(beatmap);
BeatmapInfo info = new BeatmapInfo(asset,difficulty);
Beatmap map = ScriptableObject.CreateInstance<Beatmap>();
map.metadata.title = songName;
map.metadata.titleUnicode = songName;
map.metadata.artist = "Not You";
map.metadata.artistUnicode = "Not You";
map.metadata.tagData.Level = 10;
//map.metadata.tagData.SongLength = float.Parse(beatmap.Substring(beatmap.IndexOf("SongLength\":")+13,beatmap.IndexOf("}", beatmap.IndexOf("SongLength\":") + 13)));
BeatmapItem item = new BeatmapItem();
item.Path = path;
item.BeatmapInfo = info;
item.Beatmap = map;
item.Highscore = new HighScoreItem(path, 0, 0f, 0, cleared: false, new Dictionary<string, int>(), Modifiers.None);
item.Unlocked = true;
item.Song = mapSong;
LoggerInstance.Msg("about to parse beatmap");
BeatmapParserEngine engine = new BeatmapParserEngine();
engine.ReadBeatmap(info.text, ref map, BeatmapParserEngine.SectionTypes.Everything);
songList.Add(item);
songDatabase.TryAdd(songName, item);
LoggerInstance.Msg("added " + songName + " " + difficulty);
/*public string Path;
public BeatmapIndex.Song Song;
public BeatmapInfo BeatmapInfo;
public Beatmap Beatmap;
public HighScoreItem Highscore;
public bool Unlocked;
public override string ToString()
{
return Path;
}*/
}
}
}
}