Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: BGM Continuous Stream in Duels So We Can Effectively Use Less Than 3 Tracks #340

Open
Merik2013 opened this issue Feb 25, 2024 · 13 comments

Comments

@Merik2013
Copy link
Contributor

Presently, we can set a duel to use 3 bgm tracks. One at the start of the duel, one when a keycard is played (these tend to have cut-in animations), and one when the duel is about to end (usually when you enter battle phase with enough damage on board to close the game out). However, if I want a duel that never changes the music or continues the keycard music through to the end of the duel my only option is to set the same track on multiple categories and deal with it starting the track over.

Would it be possible to add code that makes it continue to stream a track instead of starting it over if its told to play the same track its already playing?

@Merik2013 Merik2013 changed the title Feature Request Feature Request: BGM Continuous Stream in Duels So We Can Effectively Use Less Than 3 Tracks Feb 25, 2024
@pixeltris
Copy link
Owner

Did you try putting just 1 value in the audio array instead of 3?

@Merik2013
Copy link
Contributor Author

Merik2013 commented Feb 25, 2024

Yeah. The duel just fails to load. It does the infinite loading thing if you dont put three tracks in. Right now, the way it works is you need to list three tracks. The first is used as the Normal track, the second is the Keycard track, and the third is the climax track. It doesnt care if you use a normal track as the climax track or vice versa and it doesnt care if you list the same track multiple times, but it freaks out if you dont list at least three tracks.

@pixeltris
Copy link
Owner

pixeltris commented Feb 25, 2024

You'd need to hook YgomGame.Duel.Sound.PlayBGM(Sound.DuelBGM idx) which takes this enum:

public enum DuelBGM
{
	DuelEarly,
	DuelMiddle,
	DuelLate,
	DuelStart = -1
}

Inside the hook you'd need to do something like:

static void PlayBGM(IntPtr thisPtr, int idx)
{
    if (idx != 0 && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.SingleBgm"))
    {
        return;
    }
    hookPlayBGM.Original(thisPtr, idx);
}

And you'd add SingleBgm to https://github.com/pixeltris/YgoMaster/blob/master/YgoMasterServer/DuelSettings.cs

Then hook it up to your .json file with an entry like "SingleBgm":true. This would make it so only the first provided BGM would ever get played. If you wanted to do anything more complex you'd need to change the above logic.

@Merik2013
Copy link
Contributor Author

Sorry, so, what file do I place the hook in? which file is used to implement the audio system for the purposes of this change?

@Merik2013
Copy link
Contributor Author

Merik2013 commented Feb 28, 2024

also, ChatGPT was able to produce this after feeding it a prompt on everything I could want this script to do. How's this look to you? What would need changing to make it function or is it already valid?

public enum BgmMode
{
    Normal, // Default behavior
    SingleBgm, // Only the first track plays
    DoubleBgm, // First two tracks play in sequence, but not the third
    NoKeycardBGM // Allows DuelEarly to transition to DuelLate, skipping DuelMiddle
}

public class CustomAudioManager
{
    private BgmMode currentBgmMode = BgmMode.Normal;
    private string currentTrack = "DuelEarly"; // Assume starting with the first track

    public void SetBgmMode(BgmMode mode)
    {
        currentBgmMode = mode;

        // Additional logic to reset or adjust audio state if necessary
    }

    // This method should be called in place of the game's usual method for changing tracks
    public void ChangeTrack(string newTrack)
    {
        switch (currentBgmMode)
        {
            case BgmMode.SingleBgm:
                // Allow the first track to continue playing without change
                break;
            case BgmMode.DoubleBgm:
                if (currentTrack == "DuelEarly" && newTrack == "DuelMiddle")
                {
                    // Allow change to the second track
                    currentTrack = newTrack;
                    // Implement the actual track change logic here
                }
                // Prevent changing to the third track if currently on the second
                break;
            case BgmMode.NoKeycardBGM:
                if (currentTrack == "DuelEarly" && newTrack == "DuelMiddle")
                {
                    // Skip DuelMiddle, do nothing here to bypass it
                }
                else if (currentTrack == "DuelEarly" && newTrack == "DuelLate")
                {
                    // Allow transition from DuelEarly directly to DuelLate
                    currentTrack = newTrack;
                    // Implement the actual track change logic here
                }
                break;
            case BgmMode.Normal:
                // Default behavior, allow all changes
                currentTrack = newTrack;
                // Implement the actual track change logic here
                break;
        }
    }

    // Additional methods as needed...
}

@pixeltris
Copy link
Owner

which file is used to implement the audio system

Probably a new file would make sense for doing audio stuff.

How's this look to you?

That code doesn't do anything. I imagine this is outside the limits of what AI code generation can do.

@Merik2013
Copy link
Contributor Author

Then this is probably something you'll have to write the code for yourself, since I know infinitely less about C# then that ai. You can probably tell what I was trying to make it do. Three settings to cover three possible scenarios. The first you know; making it just play the first track. The second allows it to switch over to the keycard track, but not switch to the climax track, and the third just skips over the keycard track.

@Merik2013
Copy link
Contributor Author

gave GPT another go (apparently success with it depends on my prompting skills)
give this a proofread:

static void PlayBGM(IntPtr thisPtr, int idx)
{
    // Check for SingleBGM setting
    if (idx != (int)Sound.DuelBGM.DuelStart && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.SingleBgm"))
    {
        // If SingleBGM is true, prevent any music changes except for the first track
        if (idx != (int)Sound.DuelBGM.DuelEarly)
        {
            return; // Skip changing the BGM
        }
    }

    // Check for DoubleBGM setting
    if (idx == (int)Sound.DuelBGM.DuelLate && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.DoubleBgm"))
    {
        // If DoubleBGM is true, prevent changing to the third track
        return; // Skip changing the BGM
    }

    // Check for NoKeycardBGM setting
    if (idx == (int)Sound.DuelBGM.DuelMiddle && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.NoKeycardBgm"))
    {
        // If NoKeycardBGM is true, prevent changing to the second track
        return; // Skip changing the BGM
    }

    // If none of the conditions are met, proceed with the original PlayBGM call
    hookPlayBGM.Original(thisPtr, idx);
}

@Merik2013
Copy link
Contributor Author

refined it a bit after referencing the enum and got this.

static void PlayBGM(IntPtr thisPtr, int idx)
{
    // Check for SingleBGM setting when attempting to switch away from DuelEarly
    if (idx != 0 && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.SingleBgm"))
    {
        return; // Skip changing the BGM
    }

    // For DoubleBGM, prevent changing to DuelLate
    if (idx == 2 && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.DoubleBgm"))
    {
        return; // Skip changing the BGM to DuelLate
    }

    // For NoKeycardBGM, prevent changing to DuelMiddle
    if (idx == 1 && YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.NoKeycardBgm"))
    {
        return; // Skip changing the BGM to DuelMiddle
    }

    // If none of the conditions are met, proceed with the original PlayBGM call
    hookPlayBGM.Original(thisPtr, idx);
}

@pixeltris
Copy link
Owner

Yea that code sounds about right.

@Merik2013
Copy link
Contributor Author

Right, so now I need to figure out how to do the hook. After that I can make a build for testing. Given my limited knowledge I'm not sure how to recognize or correct any mistakes ChatGPT makes with the hook, but I'll keep tinkering. I assume detours can be used here since you have the library in the files.
Also, you said this would be best as its own file, but which folder should host it? Server, Client, or Loader?

For reference, this is what ChatGPT currently thinks this is supposed to look like (I don't have a real reference point, so I remain unconvinced):

using System;
using System.Runtime.InteropServices;

public static class AudioHook
{
    // Delegate matching the signature of YgomGame.Duel.Sound.PlayBGM
    private delegate void PlayBGMDelegate(Sound.DuelBGM idx);

    // Pointer to the original PlayBGM method
    private static IntPtr originalPlayBGMPtr = IntPtr.Zero;

    // Delegate instance for the original PlayBGM method
    private static PlayBGMDelegate originalPlayBGMDelegate;

    // Hook setup method
    public static void Initialize()
    {
        // Assuming GetMethodPointer obtains a pointer to the original PlayBGM method
        originalPlayBGMPtr = GetMethodPointer(typeof(YgomGame.Duel.Sound), "PlayBGM");

        // Create a delegate for the original method
        originalPlayBGMDelegate = (PlayBGMDelegate)Marshal.GetDelegateForFunctionPointer(originalPlayBGMPtr, typeof(PlayBGMDelegate));

        // Create a delegate for your detour method
        PlayBGMDelegate detourDelegate = new PlayBGMDelegate(DetouredPlayBGM);

        // Apply the hook
        Hook.Apply(originalPlayBGMPtr, detourDelegate);
    }

    // Your custom logic replacing the original PlayBGM method
    private static void DetouredPlayBGM(Sound.DuelBGM idx)
    {
        // Implement your conditions based on SingleBGM, DoubleBGM, and NoKeycardBGM
        // For example:
        if (YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.SingleBgm") && idx != Sound.DuelBGM.DuelEarly)
        {
            // If SingleBGM is true, block switching away from the first track
            return;
        }

        // Optionally call the original method if none of the conditions are met
        originalPlayBGMDelegate(idx);
    }

    // Method to obtain a pointer to the original method
    // This is a placeholder and needs to be implemented based on your hooking mechanism
    private static IntPtr GetMethodPointer(Type type, string methodName)
    {
        // Implementation to obtain the method pointer
        return IntPtr.Zero;
    }
}

@Merik2013
Copy link
Contributor Author

Merik2013 commented Mar 6, 2024

okay, so ignoring detours for now, as it seems I'd have to rewrite the code a bit for that, my understanding now is that I'll have to make a delegate entry and then have my code reference the delegate entry.
something like

delegate void PlayBGMDelegate(IntPtr thisPtr, Sound.DuelBGM idx);

Which I assume can be placed in the AudioLoader.css file somewhere? You have a bunch of other delegate entries there, so idk.
After that I need to make a new file that has this in it:

static class SoundInterceptor
{
    public static PlayBGMDelegate OriginalPlayBGM { get; set; }

    // This method acts as a proxy to the original PlayBGM method.
    public static void PlayBGM(IntPtr thisPtr, int idx)
    {
        bool SingleBGM = YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.SingleBgm");
        bool DoubleBGM = YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.DoubleBgm");
        bool NoKeycardBGM = YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.NoKeycardBgm");

        // Check for SingleBGM
        if (SingleBGM && idx != 0)
        {
            // If SingleBGM is true and the idx is not DuelEarly, skip changing the BGM
            return;
        }

        // Check for DoubleBGM
        if (DoubleBGM && idx == 2)
        {
            // If DoubleBGM is true and the idx is DuelLate, skip changing the BGM
            return;
        }

        // Check for NoKeycardBGM
        if (NoKeycardBGM && idx == 1)
        {
            // If NoKeycardBGM is true and the idx is DuelMiddle, skip changing the BGM
            return;
        }

        // If none of the flags are set, or if the conditions for SingleBGM and DoubleBGM are not met,
        // call the original PlayBGM method to maintain the game's default behavior.
        OriginalPlayBGM?.Invoke(thisPtr, (Sound.DuelBGM)idx);
    }
}

I'm just not sure if this goes into the client, the server, or the loader.
I'm also still aware that I need to plug the booleans into DuelSettings.cs and add the entries into a duel file if I want to use them, but that comes later.

@Merik2013
Copy link
Contributor Author

Merik2013 commented Mar 10, 2024

Where I am, currently:

using System;
using System.Runtime.InteropServices;
using IL2CPP;

namespace YgoMasterClient
{
    // Delegate matching the signature of YgomGame.Duel.Sound.PlayBGM
    public delegate void PlayBGMDelegate(IntPtr thisPtr, Sound.DuelBGM idx);

    static class SoundInterceptor
    {
        private static Hook<PlayBGMDelegate> hookPlayBGM;

        public static void Initialize()
        {
            // Retrieve the assembly, class, and method information
            IL2Assembly gameAssembly = Assembler.GetAssembly("GameAssembly"); // Replace "GameAssembly" with the actual name if different
            IL2Class soundClass = gameAssembly.GetClass("Sound", "YgomGame.Duel");
            IL2Method playBGMMethod = soundClass.GetMethodByName("PlayBGM", 1); // Assumes PlayBGM takes one parameter
            
            // Ensure we successfully retrieved the method
            if (playBGMMethod == null || playBGMMethod.ptr == IntPtr.Zero)
            {
                Console.WriteLine("Failed to find PlayBGM method.");
                return;
            }
            
            // Set up the hook using the method pointer
            hookPlayBGM = new Hook<PlayBGMDelegate>(DetourPlayBGM, playBGMMethod.ptr);

            // Additional steps might be required to activate or apply the hook, depending on your Hook<T> implementation
        }

        // This is the method that will be called instead of the original PlayBGM method
        private static void DetourPlayBGM(IntPtr thisPtr, Sound.DuelBGM idx)
        {
            bool SingleBGM = YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.SingleBgm");
            bool DoubleBGM = YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.DoubleBgm");
            bool NoKeycardBGM = YgomSystem.Utility.ClientWork.GetByJsonPath<bool>("Duel.NoKeycardBgm");

            // Implement your logic for when to call the original method
            if (SingleBGM && idx != 0)
            {
                return;
            }

            if (DoubleBGM && idx == 2)
            {
                return;
            }

            if (NoKeycardBGM && idx == 1)
            {
                return;
            }

            // Call the original method if none of the conditions are met
            hookPlayBGM.Original(thisPtr, idx);
        }
    }
}

I've prepared this SoundInterceptor.cs file to perform the task, but when I tried to run the server it returns this error:

[ERROR] Loading data threw an exception
System.InvalidCastException: Object must implement IConvertible.
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   at YgoMaster.DuelSettings.FromDictionary(Dictionary`2 data)
   at YgoMaster.GameServer.LoadSoloDuels()
   at YgoMaster.GameServer.LoadSolo()
   at YgoMaster.GameServer.LoadSettings()
   at YgoMaster.GameServer.Start()

It seems just adding the bool entries into DuelSettings.cs wasn't enough as the current FromDictionary implementation is somehow misinterpreting them. My attempts at generating a remedy with ChatGPT have just led to more problems. I just keep getting more and more errors at the compilation stage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants