Skip to content

Commit 4f3a12a

Browse files
authored
Merge pull request #22 from schwarper/Resolution
feat: Add MySQL resolution saving to database - Players' resolution choices are now saved to the database. - Defaults to default if no database credentials are set. refactor: Switched to SteamID for player and menu settings - Active menu and resolution now use player.SteamID instead of player.Handle. feat: Added CreateMenu and MenuByType commands - Simplifies dynamic menu creation in plugins. feat: Exposed ResolutionMenu publicly - Plugins can now display the resolution menu with any MenuType. fix: Adjust background size correctly
2 parents cc03472 + e035cef commit 4f3a12a

9 files changed

Lines changed: 257 additions & 47 deletions

File tree

API/Class/Config.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class Cfg
2121
Exit = "Tab"
2222
};
2323
public Sound Sound { get; set; } = new();
24+
public MySQL MySQL { get; set; } = new();
2425
public ChatMenuSettings ChatMenu { get; set; } = new()
2526
{
2627
TitleColor = ChatColors.Yellow,
@@ -88,6 +89,15 @@ public class Sound
8889
public string ScrollDown { get; set; } = string.Empty;
8990
}
9091

92+
public class MySQL
93+
{
94+
public string Host { get; set; } = string.Empty;
95+
public string Name { get; set; } = string.Empty;
96+
public string User { get; set; } = string.Empty;
97+
public string Pass { get; set; } = string.Empty;
98+
public uint Port { get; set; } = 3306;
99+
}
100+
91101
public class ChatMenuSettings
92102
{
93103
public char TitleColor { get; set; }
@@ -165,6 +175,7 @@ public static void LoadConfig()
165175
throw new FileNotFoundException($"Configuration file not found: {ConfigFilePath}");
166176

167177
_isSet = true;
178+
168179
string configText = File.ReadAllText(ConfigFilePath);
169180
TomlTable model = Toml.ToModel(configText);
170181

@@ -179,6 +190,12 @@ public static void LoadConfig()
179190
model.SetIfPresent("Sound.ScrollUp", (string value) => Config.Sound.ScrollUp = value);
180191
model.SetIfPresent("Sound.ScrollDown", (string value) => Config.Sound.ScrollDown = value);
181192

193+
model.SetIfPresent("MySQL.Host", (string value) => Config.MySQL.Host = value);
194+
model.SetIfPresent("MySQL.Name", (string value) => Config.MySQL.Name = value);
195+
model.SetIfPresent("MySQL.User", (string value) => Config.MySQL.User = value);
196+
model.SetIfPresent("MySQL.Pass", (string value) => Config.MySQL.Pass = value);
197+
model.SetIfPresent("MySQL.Port", (uint value) => Config.MySQL.Port = value);
198+
182199
model.SetIfPresent("ChatMenu.TitleColor", (string value) => Config.ChatMenu.TitleColor = value.GetChatColor());
183200
model.SetIfPresent("ChatMenu.EnabledColor", (string value) => Config.ChatMenu.EnabledColor = value.GetChatColor());
184201
model.SetIfPresent("ChatMenu.DisabledColor", (string value) => Config.ChatMenu.DisabledColor = value.GetChatColor());
@@ -259,5 +276,7 @@ public static void LoadConfig()
259276
}
260277
}
261278
});
279+
280+
Database.CreateDatabase();
262281
}
263282
}

API/Class/Database.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using Dapper;
2+
using MySqlConnector;
3+
using System.Data.Common;
4+
using static CS2MenuManager.API.Class.ConfigManager;
5+
6+
namespace CS2MenuManager.API.Class;
7+
8+
internal static class Database
9+
{
10+
private static readonly string GlobalDatabaseConnectionString = string.Empty;
11+
internal static readonly bool IsMYSQLSet;
12+
13+
static Database()
14+
{
15+
List<string> credentials = [
16+
Config.MySQL.Host,
17+
Config.MySQL.Name,
18+
Config.MySQL.User,
19+
Config.MySQL.Pass
20+
];
21+
22+
if (credentials.Any(string.IsNullOrEmpty))
23+
{
24+
IsMYSQLSet = false;
25+
GlobalDatabaseConnectionString = string.Empty;
26+
return;
27+
}
28+
29+
GlobalDatabaseConnectionString = new MySqlConnectionStringBuilder
30+
{
31+
Server = credentials[0],
32+
Database = credentials[1],
33+
UserID = credentials[2],
34+
Password = credentials[3],
35+
Port = Config.MySQL.Port,
36+
Pooling = true,
37+
MinimumPoolSize = 0,
38+
MaximumPoolSize = 640,
39+
ConnectionIdleTimeout = 30,
40+
AllowZeroDateTime = true
41+
}.ConnectionString;
42+
43+
IsMYSQLSet = true;
44+
}
45+
46+
public static async Task<MySqlConnection> ConnectAsync()
47+
{
48+
MySqlConnection connection = new(GlobalDatabaseConnectionString);
49+
await connection.OpenAsync();
50+
return connection;
51+
}
52+
53+
public static void CreateDatabase()
54+
{
55+
if (!IsMYSQLSet)
56+
return;
57+
58+
Task.Run(CreateDatabaseAsync);
59+
}
60+
61+
public static async Task CreateDatabaseAsync()
62+
{
63+
using DbConnection connection = await ConnectAsync();
64+
65+
await connection.ExecuteAsync(@"
66+
CREATE TABLE IF NOT EXISTS cs2_menu_manager (
67+
id INT AUTO_INCREMENT PRIMARY KEY,
68+
PositionX FLOAT NOT NULL,
69+
PositionY FLOAT NOT NULL,
70+
SteamID BIGINT UNSIGNED NOT NULL UNIQUE
71+
);
72+
");
73+
}
74+
75+
public static async Task<(float PositionX, float PositionY)> Select(ulong SteamID)
76+
{
77+
using DbConnection connection = await ConnectAsync();
78+
79+
(float, float)? row = await connection.QueryFirstOrDefaultAsync<(float, float)?>(
80+
"SELECT PositionX, PositionY FROM cs2_menu_manager WHERE SteamID = @SteamID;",
81+
new { SteamID });
82+
83+
if (row.HasValue)
84+
return row.Value;
85+
86+
ResolutionManager.Resolution defaultResolution = ResolutionManager.GetDefaultResolution();
87+
await Insert(SteamID, defaultResolution.PositionX, defaultResolution.PositionY);
88+
return (defaultResolution.PositionX, defaultResolution.PositionY);
89+
}
90+
91+
public static async Task Insert(ulong SteamID, float PositionX, float PositionY)
92+
{
93+
using MySqlConnection connection = await ConnectAsync();
94+
95+
await connection.ExecuteAsync(@"
96+
INSERT INTO cs2_menu_manager (PositionX, PositionY, SteamID)
97+
VALUES (@PositionX, @PositionY, @SteamID)
98+
ON DUPLICATE KEY UPDATE PositionX = VALUES(PositionX), PositionY = VALUES(PositionY);
99+
", new { PositionX, PositionY, SteamID });
100+
}
101+
}

API/Class/MenuManager.cs

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace CS2MenuManager.API.Class;
1010
/// </summary>
1111
public static class MenuManager
1212
{
13-
private static readonly Dictionary<IntPtr, (IMenuInstance Instance, Timer? Timer)> ActiveMenus = [];
13+
private static readonly Dictionary<ulong, (IMenuInstance Instance, Timer? Timer)> ActiveMenus = [];
1414

1515
/// <summary>
1616
/// Gets the active menu for the specified player.
@@ -19,7 +19,7 @@ public static class MenuManager
1919
/// <returns>The active menu instance, or null if no menu is active.</returns>
2020
public static IMenuInstance? GetActiveMenu(CCSPlayerController player)
2121
{
22-
return ActiveMenus.TryGetValue(player.Handle, out (IMenuInstance Instance, Timer? Timer) value) ? value.Instance : null;
22+
return ActiveMenus.TryGetValue(player.SteamID, out (IMenuInstance Instance, Timer? Timer) value) ? value.Instance : null;
2323
}
2424

2525
/// <summary>
@@ -28,22 +28,22 @@ public static class MenuManager
2828
/// <param name="player">The player controller.</param>
2929
public static void CloseActiveMenu(CCSPlayerController player)
3030
{
31-
if (ActiveMenus.TryGetValue(player.Handle, out (IMenuInstance Instance, Timer? Timer) value))
31+
if (ActiveMenus.TryGetValue(player.SteamID, out (IMenuInstance Instance, Timer? Timer) value))
3232
{
3333
value.Instance.Close();
3434
value.Timer?.Kill();
3535
value.Timer = null;
36-
ActiveMenus.Remove(player.Handle);
36+
ActiveMenus.Remove(player.SteamID);
3737
}
3838
}
3939

4040
internal static void DisposeActiveMenu(CCSPlayerController player)
4141
{
42-
if (ActiveMenus.TryGetValue(player.Handle, out (IMenuInstance Instance, Timer? Timer) value))
42+
if (ActiveMenus.TryGetValue(player.SteamID, out (IMenuInstance Instance, Timer? Timer) value))
4343
{
4444
value.Timer?.Kill();
4545
value.Timer = null;
46-
ActiveMenus.Remove(player.Handle);
46+
ActiveMenus.Remove(player.SteamID);
4747
}
4848
}
4949

@@ -90,28 +90,49 @@ public static void OpenMenu<TMenu>(CCSPlayerController player, TMenu menu, int?
9090
menu.Plugin.AddTimer(menu.MenuTime, () => CloseActiveMenu(player)) :
9191
null;
9292

93-
ActiveMenus[player.Handle] = (instance, timer);
93+
ActiveMenus[player.SteamID] = (instance, timer);
9494
instance.Display();
9595
}
9696

9797
/// <summary>
98-
/// Represent an instance of a menu of type <typeparamref name="T"/>.
98+
/// Handles key press events for the active menu of the specified player.
99+
/// </summary>
100+
/// <param name="player">The player controller.</param>
101+
/// <param name="key">The key that was pressed.</param>
102+
public static void OnKeyPress(CCSPlayerController player, int key)
103+
{
104+
GetActiveMenu(player)?.OnKeyPress(player, key);
105+
}
106+
107+
/// <summary>
108+
/// Creates a menu instance of specified type
99109
/// </summary>
100-
/// <typeparam name="T">The type of the menu to create, which must implement <see cref="IMenu"/>.</typeparam>
110+
/// <typeparam name="T">Type of menu to create (must implement BaseMenu)</typeparam>
101111
/// <param name="title">The title of the menu.</param>
102112
/// <param name="plugin">The plugin associated with the menu.</param>
103-
public static T CreateMenu<T>(string title, BasePlugin plugin) where T : IMenu
113+
/// <returns>New menu instance of requested type</returns>
114+
public static T CreateMenu<T>(string title, BasePlugin plugin) where T : BaseMenu
104115
{
105-
return (T)Activator.CreateInstance(typeof(T), title, plugin)!;
116+
return (T)MenuByType(typeof(T), title, plugin);
106117
}
107118

108119
/// <summary>
109-
/// Handles key press events for the active menu of the specified player.
120+
/// Creates a menu instance based on Type parameter
110121
/// </summary>
111-
/// <param name="player">The player controller.</param>
112-
/// <param name="key">The key that was pressed.</param>
113-
public static void OnKeyPress(CCSPlayerController player, int key)
122+
/// <param name="menuType">Type of menu to create</param>
123+
/// <param name="title">The title of the menu.</param>
124+
/// <param name="plugin">The plugin associated with the menu.</param>
125+
/// <returns>New menu instance of requested type</returns>
126+
public static IMenu MenuByType(Type menuType, string title, BasePlugin plugin)
114127
{
115-
GetActiveMenu(player)?.OnKeyPress(player, key);
128+
return menuType switch
129+
{
130+
Type t when t == typeof(ChatMenu) => new ChatMenu(title, plugin),
131+
Type t when t == typeof(ConsoleMenu) => new ConsoleMenu(title, plugin),
132+
Type t when t == typeof(CenterHtmlMenu) => new CenterHtmlMenu(title, plugin),
133+
Type t when t == typeof(WasdMenu) => new WasdMenu(title, plugin),
134+
Type t when t == typeof(ScreenMenu) => new ScreenMenu(title, plugin),
135+
_ => throw new ArgumentException($"Unsupported menu type: {menuType.FullName}", nameof(menuType))
136+
};
116137
}
117138
}

API/Class/ResolutionManager.cs

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,108 @@
11
using CounterStrikeSharp.API.Core;
2+
using CS2MenuManager.API.Interface;
23
using static CS2MenuManager.API.Class.ConfigManager;
4+
using static CS2MenuManager.API.Class.Database;
35

46
namespace CS2MenuManager.API.Class;
57

6-
internal static class ResolutionManager
8+
/// <summary>
9+
/// Provides functionality to manage and store screen resolution preferences for players.
10+
/// </summary>
11+
public static class ResolutionManager
712
{
13+
private static readonly Dictionary<ulong, Resolution> Resolutions = [];
14+
15+
/// <summary>
16+
/// Represents a player's screen resolution settings including menu position coordinates.
17+
/// </summary>
818
public class Resolution
919
{
20+
/// <summary>
21+
/// The X-coordinate position of the menu on screen.
22+
/// Default value is loaded from configuration.
23+
/// </summary>
1024
public float PositionX = Config.ScreenMenu.PositionX;
25+
26+
/// <summary>
27+
/// The Y-coordinate position of the menu on screen.
28+
/// Default value is loaded from configuration.
29+
/// </summary>
1130
public float PositionY = Config.ScreenMenu.PositionY;
1231
}
1332

14-
public static readonly Dictionary<IntPtr, Resolution> Resolutions = [];
15-
33+
/// <summary>
34+
/// Gets the default resolution settings as defined in the configuration.
35+
/// </summary>
36+
/// <returns>A new Resolution instance with default values.</returns>
1637
public static Resolution GetDefaultResolution()
1738
{
1839
return new();
1940
}
2041

42+
/// <summary>
43+
/// Retrieves the resolution settings for a specific player.
44+
/// </summary>
45+
/// <param name="player">The player controller.</param>
46+
/// <returns>
47+
/// The player's custom resolution if available, otherwise returns default resolution.
48+
/// Will query database if MySQL is configured and player settings aren't cached.
49+
/// </returns>
2150
public static Resolution GetPlayerResolution(CCSPlayerController player)
2251
{
23-
return Resolutions.TryGetValue(player.Handle, out Resolution? resolution) ? resolution : GetDefaultResolution();
52+
if (Resolutions.TryGetValue(player.SteamID, out Resolution? resolution) && resolution != null)
53+
return resolution;
54+
55+
if (!IsMYSQLSet)
56+
return GetDefaultResolution();
57+
58+
(float PositionX, float PositionY) = Select(player.SteamID).GetAwaiter().GetResult();
59+
resolution = new Resolution() { PositionX = PositionX, PositionY = PositionY };
60+
Resolutions[player.SteamID] = resolution;
61+
62+
return resolution;
2463
}
2564

65+
/// <summary>
66+
/// Updates or sets the resolution settings for a player.
67+
/// </summary>
68+
/// <param name="player">The player controller.</param>
69+
/// <param name="resolution">The resolution settings to apply.</param>
70+
/// <remarks>
71+
/// This method updates the local cache immediately and asynchronously updates
72+
/// the database if MySQL is configured.
73+
/// </remarks>
2674
public static void SetPlayerResolution(CCSPlayerController player, Resolution resolution)
2775
{
28-
Resolutions[player.Handle] = resolution;
76+
Resolutions[player.SteamID] = resolution;
77+
78+
if (IsMYSQLSet)
79+
Task.Run(async () => await Insert(player.SteamID, resolution.PositionX, resolution.PositionY));
80+
}
81+
82+
/// <summary>
83+
/// Creates and configures a resolution selection menu of the specified type.
84+
/// </summary>
85+
/// <typeparam name="T">The type of menu to create, must inherit from <see cref="BaseMenu"/></typeparam>
86+
/// <param name="player">The player controller this menu is for</param>
87+
/// <param name="plugin">The plugin instance that owns this menu</param>
88+
/// <param name="prevMenu">The previous menu to return to after selection</param>
89+
/// <returns>A configured menu instance of type <typeparamref name="T"/></returns>
90+
public static T ResolutionMenu<T>(CCSPlayerController player, BasePlugin plugin, IMenu? prevMenu) where T : BaseMenu
91+
{
92+
T menu = MenuManager.CreateMenu<T>(player.Localizer("SelectResolution"), plugin);
93+
94+
if (menu is Menu.ScreenMenu screenMenu)
95+
screenMenu.ShowResolutionsOption = false;
96+
97+
foreach (KeyValuePair<string, Resolution> resolution in Config.Resolutions)
98+
{
99+
menu.AddItem(resolution.Key, (p, o) =>
100+
{
101+
SetPlayerResolution(p, resolution.Value);
102+
prevMenu?.Display(p, prevMenu.MenuTime);
103+
});
104+
}
105+
106+
return menu;
29107
}
30108
}

0 commit comments

Comments
 (0)