roflmuffin / CounterStrikeSharp

CounterStrikeSharp allows you to write server plugins in C# for Counter-Strike 2/Source2/CS2
https://docs.cssharp.dev
Other
714 stars 111 forks source link

[DRAFT] Change `BaseMenu` to support third party menu implementations #422

Open KillStr3aK opened 2 months ago

KillStr3aK commented 2 months ago

Had a couple of free hours and decided to implement this, it might come handy for some developers also with the hope that it will make it way easier for us to work with menus in general.

I could not test it that much but it was working fine for me but I'd like to request some testing before merging this just to make sure everything is in place.

this menu type is very customizable, mostly everything can be changed but it comes with the cons of the fact that we can't have that much options displayed in general.

developers can change the default button strings by changing the formatter they want with their own implementation, using this they also can use the localizer for them, or style it to their likings

the main part was to get some more control over the center html menu, we can either set any button from the PlayerButtons enum for the actions, or we can implement a custom resolver (lets say bind commands) to select and invoke an option callback

[!NOTE]
BY DEFAULT:

  • SPACE: Invoke Callback
  • W: Up
  • S: Down

here are some examples: (dont mind the colors, you can change them)

// basic
this.AddCommand("html1", "test1 dynamic html menu", (player, info) =>
{
    if (player == null || !player.IsValid)
        return;

    DynamicHtmlMenu menu = new DynamicHtmlMenu("DynamicHtmlMenuTest", this)
    {
        SelectedFormatter = (text) => { return $"► {text} ◄"; }, // custom formatter for the selected option
        SelectedSound = null // VERY LOUD | developers can set any sound file
    };

    menu.AddMenuOption($"BIG", (player, option) =>
    {
        player.PrintToChat($"BIG");
    });

    menu.AddMenuOption($"SMALL", (player, option) =>
    {
        player.PrintToChat($"SMALL");
    });

    menu.AddMenuOption($"MEDIUM", (player, option) =>
    {
        player.PrintToChat($"MEDIUM");
    });

    menu.Open(player);
});

image

// custom resolver with commands

this.AddCommand("html2", "test2 dynamic html menu", (player, info) =>
{
    if (player == null || !player.IsValid)
        return;

    DynamicHtmlMenu menu = new DynamicHtmlMenu("DynamicHtmlMenuTest2", this)
    {
        SelectedFormatter = (text) => { return $"<font color='gold'>► </font><font color='red'>{text}</font><font color='gold'> ◄</font>"; },
        SelectedSound = null,
        CustomResolver = true,
        FreezePlayer = false
    };

    menu.AddMenuOption($"BIG", (player, option) =>
    {
        player.PrintToChat($"BIG");
    });

    menu.AddMenuOption($"SMALL", (player, option) =>
    {
        player.PrintToChat($"SMALL");
    });

    menu.AddMenuOption($"MEDIUM", (player, option) =>
    {
        player.PrintToChat($"MEDIUM");
    });

    menu.Open(player);
});

// these could be anything really, i made random commands as an example but you could do binds or whatever to resolve
this.AddCommand("resolveup", "custom resolver up", (player, info) =>
{
    if (player == null || !player.IsValid)
        return;

    DynamicHtmlMenuInstance? menu = MenuManager.GetActiveMenu(player) as DynamicHtmlMenuInstance;

    if (menu == null)
    {
        player.PrintToCenter("no active menu");
        return;
    }

    menu.SelectPreviousOption();
});

this.AddCommand("resolvedown", "custom resolver down", (player, info) =>
{
    if (player == null || !player.IsValid)
        return;

    DynamicHtmlMenuInstance? menu = MenuManager.GetActiveMenu(player) as DynamicHtmlMenuInstance;

    if (menu == null)
    {
        player.PrintToCenter("no active menu");
        return;
    }

    menu.SelectNextOption();
});

this.AddCommand("resolveinvoke", "custom resolver invoke", (player, info) =>
{
    if (player == null || !player.IsValid)
        return;

    DynamicHtmlMenuInstance? menu = MenuManager.GetActiveMenu(player) as DynamicHtmlMenuInstance;

    if (menu == null)
    {
        player.PrintToCenter("no active menu");
        return;
    }

    menu.InvokeSelection();
});

image

if you dont want to use the "WS/WASD" navigation, you can implement your own resolver logic to navigate in the menu, kinda similar to the !1 !2 and other bind methods, you can do whatever you want

// pagination

this.AddCommand("html3", "test3 dynamic html menu", (player, info) =>
{
    if (player == null || !player.IsValid)
        return;

    DynamicHtmlMenu menu = new DynamicHtmlMenu("DynamicHtmlMenuTest3", this)
    {
        SelectedSound = null,
        FreezePlayer = false, // player wont be freezed
        PostSelectAction = PostSelectAction.Close // menu will be closed after an option is selected
    };

    menu.AddMenuOption($"HUGE AMOUNT OF BIG", (player, option) =>
    {
        player.PrintToChat($"HUGE AMOUNT OF BIG");
    });

    menu.AddMenuOption($"EXTRA BIG", (player, option) =>
    {
        player.PrintToChat($"EXTRA BIG");
    });

    menu.AddMenuOption($"BIG", (player, option) =>
    {
        player.PrintToChat($"BIG");
    });

    menu.AddMenuOption($"SMALL", (player, option) =>
    {
        player.PrintToChat($"SMALL");
    });

    menu.AddMenuOption($"EXTRA SMALL", (player, option) =>
    {
        player.PrintToChat($"EXTRA SMALL");
    });

    menu.AddMenuOption($"SMALLEST", (player, option) =>
    {
        player.PrintToChat($"SMALLEST");
    });

    menu.AddMenuOption($"MEDIUM", (player, option) =>
    {
        player.PrintToChat($"MEDIUM");
    });

    menu.Open(player);
});

image

the "page x/n" text can be toggled in the ctor properties

I did not show everything, you can take a look on the DynamicHtmlMenu and DynamicHtmlMenuInstance classes for more information, lately if it fits we can also add this to the docs

KillStr3aK commented 2 months ago

oh and also, I'm down for any other name, just this was the first thing that came to my mind when I started working on it

Nianmou commented 2 months ago

👍🏻

qstage commented 2 months ago

It would be nice if SelectPreviousOption and SelectNextOption returned bool true in case of successful transition and false in case of failure (reached the end of the menu or the beginning)

KillStr3aK commented 2 months ago

It would be nice if SelectPreviousOption and SelectNextOption returned bool true in case of successful transition and false in case of failure (reached the end of the menu or the beginning)

Indeed, will make some adjustments including this

crashzk commented 2 months ago

I have a doubt, I don't know how this version works, but in the versions I tested where plugins implemented a feature like this, even with the menu open, the W, A, S and D keys made the players move.

It would be interesting if when you open this menu these keys that we use to select don't move the player, it stays still until the menu closes again, is something like that possible?

KillStr3aK commented 2 months ago

I have a doubt, I don't know how this version works, but in the versions I tested where plugins implemented a feature like this, even with the menu open, the W, A, S and D keys made the players move.

It would be interesting if when you open this menu these keys that we use to select don't move the player, it stays still until the menu closes again, is something like that possible?

the default resolver freezes the player so they are unable to move while they are in the menu. developers also able to override this with their own resolver logic, like they let the player move around but resolve the transition between options and selecting with their implementation. (for e.g.: binds, or anything really.. you could even select a menu option through a discord bot)

KillStr3aK commented 2 months ago

a bug has been reported recently, I'll take a look on it soon