diff --git a/Database/GameDatabase.cs b/Database/GameDatabase.cs index 7d714b3..8566a41 100644 --- a/Database/GameDatabase.cs +++ b/Database/GameDatabase.cs @@ -281,7 +281,7 @@ public static class GameDatabase }, new() { Game = "Abiotic Factor", - AppID = "2816220", + AppID = "2857200", ExeName = @"AbioticFactor\Binaries\Win64\AbioticFactorServer-Win64-Shipping.exe", RequiredArgs = "{map}?Listen -log -MaxPlayers={MaxPlayers} -Port={port} -QueryPort={query} -ServerPassword=\"{pass}\" -SteamAppId={steamAppID}", RelativeConfigPath = @"AbioticFactor\Saved\Config\WindowsServer\GameUserSettings.ini", diff --git a/Design/GridStyler.cs b/Design/GridStyler.cs index ab23612..0e26c7b 100644 --- a/Design/GridStyler.cs +++ b/Design/GridStyler.cs @@ -12,6 +12,7 @@ // ============================================================================ using System.Windows.Forms.DataVisualization.Charting; using static Synix_Control_Panel.SynixEngine.Core; +using System.Drawing.Drawing2D; namespace Synix_Control_Panel.Design { @@ -35,11 +36,13 @@ public static void DarkTheme(DataGridView dgv) dgv.AutoGenerateColumns = false; // Map Columns to Class Properties + if (dgv.Columns.Contains("colIcon")) dgv.Columns["colIcon"].DataPropertyName = ""; if (dgv.Columns.Contains("colName")) dgv.Columns["colName"].DataPropertyName = "ServerName"; if (dgv.Columns.Contains("colGame")) dgv.Columns["colGame"].DataPropertyName = "Game"; if (dgv.Columns.Contains("colPort")) dgv.Columns["colPort"].DataPropertyName = "Port"; if (dgv.Columns.Contains("colStatus")) dgv.Columns["colStatus"].DataPropertyName = "Status"; - dgv.Columns["PlayerCountDisplay"].DefaultCellStyle.ForeColor = Color.Cyan; + if (dgv.Columns.Contains("colPlayerCount")) dgv.Columns["colPlayerCount"].DataPropertyName = "PlayerCount"; + if (dgv.Columns.Contains("colUptime")) dgv.Columns["colUptime"].DataPropertyName = "Uptime"; // Header Style (Kills the blue Game column) dgv.EnableHeadersVisualStyles = false; @@ -57,8 +60,246 @@ public static void DarkTheme(DataGridView dgv) } } + public static void StyleMinimizeButton(Button btn) + { + // Strip the default UI + btn.FlatStyle = FlatStyle.Flat; + btn.FlatAppearance.BorderSize = 0; + btn.BackColor = Color.Transparent; + btn.FlatAppearance.MouseOverBackColor = Color.Transparent; + btn.FlatAppearance.MouseDownBackColor = Color.Transparent; + + btn.Text = ""; + btn.TabStop = false; + + // Override the Paint event for the smooth pill shape + btn.Paint += (s, e) => + { + Button b = (Button)s; + e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + + Point mousePos = b.PointToClient(System.Windows.Forms.Cursor.Position); + bool isHovering = b.ClientRectangle.Contains(mousePos); + bool isPressed = isHovering && (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left; + + Color bgColor = Color.WhiteSmoke; + Color fgColor = Color.Black; + + // UPDATED: Much darker, highly visible gray hover colors + if (isPressed) + { + bgColor = Color.FromArgb(160, 160, 160); // Darker gray for click + } + else if (isHovering) + { + bgColor = Color.FromArgb(200, 200, 200); // Noticeable gray for hover + } + + // Draw the smooth background curve + using (var path = GetRoundedPath(b.ClientRectangle, 6)) + using (var brush = new SolidBrush(bgColor)) + { + e.Graphics.FillPath(brush, path); + } + + // Draw a perfect, crisp minimize line + int lineWidth = 12; + int lineThickness = 2; + + // Calculate exact center + int xPos = (b.Width / 2) - (lineWidth / 2); + int yPos = (b.Height / 2) - (lineThickness / 2) + 2; + + using (SolidBrush lineBrush = new SolidBrush(fgColor)) + { + e.Graphics.FillRectangle(lineBrush, xPos, yPos, lineWidth, lineThickness); + } + }; + + // Force instant redraws on mouse interaction + btn.MouseEnter += (s, e) => btn.Invalidate(); + btn.MouseLeave += (s, e) => btn.Invalidate(); + btn.MouseDown += (s, e) => btn.Invalidate(); + btn.MouseUp += (s, e) => btn.Invalidate(); + } + + public static void StyleIconButton(Button btn, Image icon, Color hoverColor) + { + // 1. Strip the default UI + btn.FlatStyle = FlatStyle.Flat; + btn.FlatAppearance.BorderSize = 0; + btn.BackColor = Color.Transparent; + btn.FlatAppearance.MouseOverBackColor = Color.Transparent; + btn.FlatAppearance.MouseDownBackColor = Color.Transparent; + + btn.Text = ""; + btn.TabStop = false; + + // 2. Override the Paint event for the smooth pill shape and image + btn.Paint += (s, e) => + { + Button b = (Button)s; + e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + + // This makes sure the PNG scales down smoothly without looking pixelated + e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + + Point mousePos = b.PointToClient(System.Windows.Forms.Cursor.Position); + bool isHovering = b.ClientRectangle.Contains(mousePos); + bool isPressed = isHovering && (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left; + + Color bgColor = Color.WhiteSmoke; // Default background to match your other buttons + + // Apply custom hover/click colors + if (isPressed) + { + bgColor = Color.DarkGray; + } + else if (isHovering) + { + bgColor = hoverColor; + } + + // Draw the smooth background curve + using (var path = GetRoundedPath(b.ClientRectangle, 6)) + using (var brush = new SolidBrush(bgColor)) + { + e.Graphics.FillPath(brush, path); + } + + // 3. Draw the Icon perfectly centered + if (icon != null) + { + // Calculate a size that fits comfortably inside the pill with a 4px padding + int iconSize = Math.Min(b.Width, b.Height) - 8; + int x = (b.Width - iconSize) / 2; + int y = (b.Height - iconSize) / 2; + + e.Graphics.DrawImage(icon, new Rectangle(x, y, iconSize, iconSize)); + } + }; + + // 4. Force instant redraws on mouse interaction + btn.MouseEnter += (s, e) => btn.Invalidate(); + btn.MouseLeave += (s, e) => btn.Invalidate(); + btn.MouseDown += (s, e) => btn.Invalidate(); + btn.MouseUp += (s, e) => btn.Invalidate(); + } + + public static void StyleCloseButton(Button btn) + { + // 1. Strip the default UI and make it perfectly transparent + btn.FlatStyle = FlatStyle.Flat; + btn.FlatAppearance.BorderSize = 0; + btn.BackColor = Color.Transparent; + btn.FlatAppearance.MouseOverBackColor = Color.Transparent; + btn.FlatAppearance.MouseDownBackColor = Color.Transparent; + + // Clear the standard text because we will draw it manually to layer it correctly + btn.Text = ""; + btn.TabStop = false; + + // 2. Override the Paint event to draw a high-quality smooth shape + btn.Paint += (s, e) => + { + Button b = (Button)s; + + // Turn on high-quality edge smoothing + e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + + // Determine if the mouse is hovering or actively clicking + Point mousePos = b.PointToClient(System.Windows.Forms.Cursor.Position); + bool isHovering = b.ClientRectangle.Contains(mousePos); + bool isPressed = isHovering && (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left; + + Color bgColor = Color.WhiteSmoke; + Color fgColor = Color.Black; + + if (isPressed) + { + bgColor = Color.FromArgb(178, 11, 22); // Dark Red (Click) + fgColor = Color.White; + } + else if (isHovering) + { + bgColor = Color.FromArgb(232, 17, 35); // Bright Red (Hover) + fgColor = Color.White; + } + + // Draw the smooth background + using (var path = GetRoundedPath(b.ClientRectangle, 6)) + using (var brush = new SolidBrush(bgColor)) + { + e.Graphics.FillPath(brush, path); + } + + // Draw the text exactly in the center + TextRenderer.DrawText( + e.Graphics, + "✕", + new Font("Segoe UI", 10, FontStyle.Bold), + b.ClientRectangle, + fgColor, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter + ); + }; + + // 3. Force the button to redraw itself instantly when the mouse interacts with it + btn.MouseEnter += (s, e) => btn.Invalidate(); + btn.MouseLeave += (s, e) => btn.Invalidate(); + btn.MouseDown += (s, e) => btn.Invalidate(); + btn.MouseUp += (s, e) => btn.Invalidate(); + } + + private static System.Drawing.Drawing2D.GraphicsPath GetRoundedPath(Rectangle rect, int radius) + { + System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath(); + int d = radius * 2; + path.StartFigure(); + path.AddArc(rect.X, rect.Y, d, d, 180, 90); + path.AddArc(rect.Width - d - 1, rect.Y, d, d, 270, 90); + path.AddArc(rect.Width - d - 1, rect.Height - d - 1, d, d, 0, 90); + path.AddArc(rect.X, rect.Height - d - 1, d, d, 90, 90); + path.CloseFigure(); + return path; + } + + public static void ApplyRoundedCorners(DataGridView dgv, int radius) + { + // Apply the rounded corners immediately + UpdateGridRegion(dgv, radius); + + // Ensure the rounded corners recalculate if the form is resized + dgv.Resize += (s, e) => UpdateGridRegion(dgv, radius); + } + + private static void UpdateGridRegion(DataGridView dgv, int radius) + { + if (dgv == null || dgv.Width == 0 || dgv.Height == 0) return; + + int diameter = radius * 2; + GraphicsPath path = new GraphicsPath(); + + path.StartFigure(); + // Top Left Corner + path.AddArc(new Rectangle(0, 0, diameter, diameter), 180, 90); + // Top Right Corner + path.AddArc(new Rectangle(dgv.Width - diameter, 0, diameter, diameter), 270, 90); + // Bottom Right Corner + path.AddArc(new Rectangle(dgv.Width - diameter, dgv.Height - diameter, diameter, diameter), 0, 90); + // Bottom Left Corner + path.AddArc(new Rectangle(0, dgv.Height - diameter, diameter, diameter), 90, 90); + path.CloseFigure(); + + // Apply the new region and dispose of the old one to prevent memory leaks + Region oldRegion = dgv.Region; + dgv.Region = new Region(path); + oldRegion?.Dispose(); + } + public static void ApplyTransparentTheme(DataGridView dgv) { + dgv.RowHeadersVisible = false; dgv.BackgroundColor = BackgroundBlack; dgv.BorderStyle = BorderStyle.None; @@ -70,6 +311,47 @@ public static void ApplyTransparentTheme(DataGridView dgv) dgv.GridColor = Color.FromArgb(45, 45, 45); dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + + // Add this line to trigger the custom border drawing + dgv.RowPostPaint -= Dgv_PaintGlowingSelection; // Prevent multiple subscriptions + dgv.RowPostPaint += Dgv_PaintGlowingSelection; + } + + private static void Dgv_PaintGlowingSelection(object sender, DataGridViewRowPostPaintEventArgs e) + { + DataGridView dgv = sender as DataGridView; + if (dgv == null) return; + + if ((e.State & DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected) + { + // 1. Get the exact width of the data columns, skipping the row header + int startX = dgv.RowHeadersVisible ? dgv.RowHeadersWidth : 0; + int width = dgv.Columns.GetColumnsWidth(DataGridViewElementStates.Visible) - dgv.HorizontalScrollingOffset; + + // 2. Define the bounds. We inset by 2 pixels so the thickest pen doesn't get clipped. + Rectangle bounds = new Rectangle(startX + 2, e.RowBounds.Y + 2, width - 5, e.RowBounds.Height - 5); + + Color neonColor = Color.DarkCyan; // The color of the glow + + // LAYER 1: The wide, faint blur (Width 5) + using (Pen outerGlow = new Pen(Color.FromArgb(40, neonColor), 5)) + { + e.Graphics.DrawRectangle(outerGlow, bounds); + } + + // LAYER 2: The tighter, brighter blur (Width 3) + using (Pen innerGlow = new Pen(Color.FromArgb(100, neonColor), 3)) + { + e.Graphics.DrawRectangle(innerGlow, bounds); + } + + // LAYER 3: The intense hot core (Width 1) + // Using White makes it look like actual glowing gas, but you can change this back to Lime if you prefer. + using (Pen corePen = new Pen(Color.White, 1)) + { + e.Graphics.DrawRectangle(corePen, bounds); + } + } } public static void PaintTransparentRows(DataGridView dgv, DataGridViewCellPaintingEventArgs e) diff --git a/FileFolderHandler/FileHandler.cs b/FileFolderHandler/FileHandler.cs index c479831..cc45aab 100644 --- a/FileFolderHandler/FileHandler.cs +++ b/FileFolderHandler/FileHandler.cs @@ -56,6 +56,7 @@ public static void LoadServers() MainGUI.serverList.Clear(); foreach (var server in loadedServers) { + // 1. Grab the hardcoded data from the switch statement in your screenshot var masterData = GameDatabase.GetGame(server.Game); if (masterData != null) { @@ -63,6 +64,18 @@ public static void LoadServers() server.ExeName = masterData.ExeName; server.RequiredArgs = masterData.RequiredArgs; server.Maps = masterData.Maps.ToList(); + + // 2. Smash the JSON path and Hardcoded ExeName together + string fullExePath = Path.Combine(server.InstallPath, server.ExeName); + + // 3. Extract the icon + string iconPath = Synix_Control_Panel.SynixEngine.Core.GetLocalServerIcon(server.AppID, fullExePath); + + // 4. Attach it permanently to the object + if (File.Exists(iconPath)) + { + server.DisplayIcon = System.Drawing.Image.FromFile(iconPath); + } } MainGUI.serverList.Add(server); } diff --git a/Images/discord.png b/Images/discord.png new file mode 100644 index 0000000..d27a305 Binary files /dev/null and b/Images/discord.png differ diff --git a/Images/github.png b/Images/github.png new file mode 100644 index 0000000..89a55a1 Binary files /dev/null and b/Images/github.png differ diff --git a/MainGUI.Designer.cs b/MainGUI.Designer.cs index 8672cc0..ca05429 100644 --- a/MainGUI.Designer.cs +++ b/MainGUI.Designer.cs @@ -34,13 +34,6 @@ private void InitializeComponent() System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series(); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainGUI)); dataGridView1 = new DataGridView(); - colGame = new DataGridViewTextBoxColumn(); - colName = new DataGridViewTextBoxColumn(); - colPort = new DataGridViewTextBoxColumn(); - colQueryPort = new DataGridViewTextBoxColumn(); - PlayerCountDisplay = new DataGridViewTextBoxColumn(); - UptimeDisplay = new DataGridViewTextBoxColumn(); - colStatus = new DataGridViewTextBoxColumn(); rtbLog = new RichTextBox(); btnStart = new Button(); btnStop = new Button(); @@ -59,8 +52,8 @@ private void InitializeComponent() toolStripSeparator5 = new ToolStripSeparator(); updateServerToolStripMenuItem = new ToolStripMenuItem(); fileValidationToolStripMenuItem = new ToolStripMenuItem(); - backupServerToolStripMenuItem = new ToolStripMenuItem(); btnExportBatch = new ToolStripMenuItem(); + backupServerToolStripMenuItem = new ToolStripMenuItem(); toolStripSeparator3 = new ToolStripSeparator(); connectionTestToolStripMenuItem = new ToolStripMenuItem(); connectionLocalTestToolStripMenuItem = new ToolStripMenuItem(); @@ -76,6 +69,18 @@ private void InitializeComponent() lblUpdateStatus = new Label(); btnDownloadUpdate = new Button(); chkPrivacyMode = new CheckBox(); + btnClose = new Button(); + btnMinimize = new Button(); + btnDiscord = new Button(); + btnGithub = new Button(); + DisplayIcon = new DataGridViewTextBoxColumn(); + colGame = new DataGridViewTextBoxColumn(); + colName = new DataGridViewTextBoxColumn(); + colPort = new DataGridViewTextBoxColumn(); + colQueryPort = new DataGridViewTextBoxColumn(); + colPlayerCount = new DataGridViewTextBoxColumn(); + colUptime = new DataGridViewTextBoxColumn(); + colStatus = new DataGridViewTextBoxColumn(); ((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit(); ((System.ComponentModel.ISupportInitialize)logo).BeginInit(); ((System.ComponentModel.ISupportInitialize)chartHeartbeat).BeginInit(); @@ -88,79 +93,25 @@ private void InitializeComponent() dataGridView1.AllowUserToDeleteRows = false; dataGridView1.BorderStyle = BorderStyle.None; dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; - dataGridView1.Columns.AddRange(new DataGridViewColumn[] { colGame, colName, colPort, colQueryPort, PlayerCountDisplay, UptimeDisplay, colStatus }); - dataGridView1.Location = new Point(12, 140); + dataGridView1.Columns.AddRange(new DataGridViewColumn[] { DisplayIcon, colGame, colName, colPort, colQueryPort, colPlayerCount, colUptime, colStatus }); + dataGridView1.Location = new Point(12, 171); + dataGridView1.MultiSelect = false; dataGridView1.Name = "dataGridView1"; dataGridView1.ReadOnly = true; - dataGridView1.Size = new Size(881, 442); + dataGridView1.Size = new Size(881, 538); dataGridView1.TabIndex = 0; dataGridView1.CellDoubleClick += dataGridView1_CellDoubleClick; dataGridView1.CellFormatting += dataGridView1_CellFormatting; dataGridView1.CellPainting += dataGridView1_CellPainting; // - // colGame - // - colGame.DataPropertyName = "Game"; - colGame.HeaderText = "Game"; - colGame.Name = "colGame"; - colGame.ReadOnly = true; - colGame.Width = 200; - // - // colName - // - colName.DataPropertyName = "ServerName"; - colName.HeaderText = "Server Name"; - colName.Name = "colName"; - colName.ReadOnly = true; - colName.Width = 220; - // - // colPort - // - colPort.DataPropertyName = "Port"; - colPort.HeaderText = "Port"; - colPort.Name = "colPort"; - colPort.ReadOnly = true; - colPort.Width = 80; - // - // colQueryPort - // - colQueryPort.DataPropertyName = "QueryPort"; - colQueryPort.HeaderText = "Query Port"; - colQueryPort.Name = "colQueryPort"; - colQueryPort.ReadOnly = true; - colQueryPort.Width = 80; - // - // PlayerCountDisplay - // - PlayerCountDisplay.DataPropertyName = "PlayerCountDisplay"; - PlayerCountDisplay.HeaderText = "Players"; - PlayerCountDisplay.Name = "PlayerCountDisplay"; - PlayerCountDisplay.ReadOnly = true; - PlayerCountDisplay.Width = 70; - // - // UptimeDisplay - // - UptimeDisplay.DataPropertyName = "UptimeDisplay"; - UptimeDisplay.HeaderText = "UPTIME"; - UptimeDisplay.Name = "UptimeDisplay"; - UptimeDisplay.ReadOnly = true; - // - // colStatus - // - colStatus.DataPropertyName = "Status"; - colStatus.HeaderText = "Status"; - colStatus.Name = "colStatus"; - colStatus.ReadOnly = true; - colStatus.Width = 90; - // // rtbLog // rtbLog.BackColor = SystemColors.ActiveCaptionText; rtbLog.ForeColor = Color.Lime; - rtbLog.Location = new Point(899, 12); + rtbLog.Location = new Point(899, 45); rtbLog.Name = "rtbLog"; rtbLog.ReadOnly = true; - rtbLog.Size = new Size(330, 605); + rtbLog.Size = new Size(330, 712); rtbLog.TabIndex = 6; rtbLog.Text = ""; // @@ -169,9 +120,9 @@ private void InitializeComponent() btnStart.Cursor = Cursors.Hand; btnStart.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); btnStart.ForeColor = Color.Green; - btnStart.Location = new Point(574, 589); + btnStart.Location = new Point(574, 726); btnStart.Name = "btnStart"; - btnStart.Size = new Size(101, 28); + btnStart.Size = new Size(101, 32); btnStart.TabIndex = 8; btnStart.Text = "🚀 Start"; btnStart.UseVisualStyleBackColor = true; @@ -182,9 +133,9 @@ private void InitializeComponent() btnStop.Cursor = Cursors.Hand; btnStop.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); btnStop.ForeColor = Color.Red; - btnStop.Location = new Point(788, 589); + btnStop.Location = new Point(788, 726); btnStop.Name = "btnStop"; - btnStop.Size = new Size(101, 28); + btnStop.Size = new Size(101, 32); btnStop.TabIndex = 9; btnStop.Text = "❌ Stop"; btnStop.UseVisualStyleBackColor = true; @@ -194,12 +145,13 @@ private void InitializeComponent() // logo.BackColor = Color.Transparent; logo.Image = Properties.Resources.synix_logo; - logo.Location = new Point(-10, -44); + logo.Location = new Point(-10, -70); logo.Name = "logo"; - logo.Size = new Size(321, 189); + logo.Size = new Size(353, 270); logo.SizeMode = PictureBoxSizeMode.StretchImage; logo.TabIndex = 10; logo.TabStop = false; + logo.MouseDown += Form_Drag_MouseDown; // // chartHeartbeat // @@ -208,13 +160,13 @@ private void InitializeComponent() chartHeartbeat.Cursor = Cursors.Hand; legend1.Name = "Legend1"; chartHeartbeat.Legends.Add(legend1); - chartHeartbeat.Location = new Point(505, 28); + chartHeartbeat.Location = new Point(505, 30); chartHeartbeat.Name = "chartHeartbeat"; series1.ChartArea = "ChartArea1"; series1.Legend = "Legend1"; series1.Name = "Series1"; chartHeartbeat.Series.Add(series1); - chartHeartbeat.Size = new Size(384, 73); + chartHeartbeat.Size = new Size(388, 98); chartHeartbeat.TabIndex = 11; chartHeartbeat.Text = "chart1"; chartHeartbeat.Click += ResourceGraph_Click; @@ -224,9 +176,9 @@ private void InitializeComponent() lblTotalRam.AutoSize = true; lblTotalRam.BackColor = Color.Transparent; lblTotalRam.ForeColor = Color.Fuchsia; - lblTotalRam.Location = new Point(694, 9); + lblTotalRam.Location = new Point(681, 9); lblTotalRam.Name = "lblTotalRam"; - lblTotalRam.Size = new Size(33, 15); + lblTotalRam.Size = new Size(36, 17); lblTotalRam.TabIndex = 12; lblTotalRam.Text = "RAM"; // @@ -236,7 +188,7 @@ private void InitializeComponent() lblTotalCpu.BackColor = Color.Transparent; lblTotalCpu.Font = new Font("Segoe UI", 9F, FontStyle.Bold, GraphicsUnit.Point, 0); lblTotalCpu.ForeColor = Color.DarkCyan; - lblTotalCpu.Location = new Point(522, 9); + lblTotalCpu.Location = new Point(522, 11); lblTotalCpu.Name = "lblTotalCpu"; lblTotalCpu.Size = new Size(30, 15); lblTotalCpu.TabIndex = 13; @@ -246,12 +198,12 @@ private void InitializeComponent() // contextMenuStrip.Items.AddRange(new ToolStripItem[] { btnHelp, openServerConfig, installServer, toolStripSeparator1 }); contextMenuStrip.Name = "contextMenuStrip"; - contextMenuStrip.Size = new Size(181, 98); + contextMenuStrip.Size = new Size(152, 76); // // btnHelp // btnHelp.Name = "btnHelp"; - btnHelp.Size = new Size(180, 22); + btnHelp.Size = new Size(151, 22); btnHelp.Text = "Help"; btnHelp.Click += btnHelp_Click; // @@ -259,7 +211,7 @@ private void InitializeComponent() // openServerConfig.DropDownItems.AddRange(new ToolStripItem[] { openServerFolderToolStripMenuItem, backupToolStripMenuItem, toolStripSeparator2, editServerToolStripMenuItem, openServerConfigFileToolStripMenuItem, toolStripSeparator5, updateServerToolStripMenuItem, fileValidationToolStripMenuItem, btnExportBatch, backupServerToolStripMenuItem, toolStripSeparator3, connectionTestToolStripMenuItem, connectionLocalTestToolStripMenuItem, toolStripSeparator4, deleteServerToolStripMenuItem }); openServerConfig.Name = "openServerConfig"; - openServerConfig.Size = new Size(180, 22); + openServerConfig.Size = new Size(151, 22); openServerConfig.Text = "Server Options"; // // openServerFolderToolStripMenuItem @@ -314,13 +266,6 @@ private void InitializeComponent() fileValidationToolStripMenuItem.Text = "Game Validation"; fileValidationToolStripMenuItem.Click += btnFileValidation_Click; // - // backupServerToolStripMenuItem - // - backupServerToolStripMenuItem.Name = "backupServerToolStripMenuItem"; - backupServerToolStripMenuItem.Size = new Size(196, 22); - backupServerToolStripMenuItem.Text = "Backup Server"; - backupServerToolStripMenuItem.Click += btnBackup_Click; - // // btnExportBatch // btnExportBatch.Name = "btnExportBatch"; @@ -328,6 +273,13 @@ private void InitializeComponent() btnExportBatch.Text = "Export to Batch File"; btnExportBatch.Click += btnExportBatch_Click; // + // backupServerToolStripMenuItem + // + backupServerToolStripMenuItem.Name = "backupServerToolStripMenuItem"; + backupServerToolStripMenuItem.Size = new Size(196, 22); + backupServerToolStripMenuItem.Text = "Backup Server"; + backupServerToolStripMenuItem.Click += btnBackup_Click; + // // toolStripSeparator3 // toolStripSeparator3.Name = "toolStripSeparator3"; @@ -362,22 +314,22 @@ private void InitializeComponent() // installServer // installServer.Name = "installServer"; - installServer.Size = new Size(180, 22); + installServer.Size = new Size(151, 22); installServer.Text = "Install Server"; installServer.Click += btnAddServer_Click; // // toolStripSeparator1 // toolStripSeparator1.Name = "toolStripSeparator1"; - toolStripSeparator1.Size = new Size(177, 6); + toolStripSeparator1.Size = new Size(148, 6); // // btnServerActions // btnServerActions.Cursor = Cursors.Hand; btnServerActions.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); - btnServerActions.Location = new Point(12, 590); + btnServerActions.Location = new Point(12, 724); btnServerActions.Name = "btnServerActions"; - btnServerActions.Size = new Size(142, 28); + btnServerActions.Size = new Size(142, 32); btnServerActions.TabIndex = 16; btnServerActions.Text = "🛠️ Server Actions"; btnServerActions.UseVisualStyleBackColor = true; @@ -395,7 +347,7 @@ private void InitializeComponent() lblLocalIP1.Cursor = Cursors.Hand; lblLocalIP1.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); lblLocalIP1.ForeColor = Color.Lime; - lblLocalIP1.Location = new Point(186, 605); + lblLocalIP1.Location = new Point(177, 746); lblLocalIP1.Name = "lblLocalIP1"; lblLocalIP1.Size = new Size(56, 17); lblLocalIP1.TabIndex = 18; @@ -409,7 +361,7 @@ private void InitializeComponent() lblPublicIP.Cursor = Cursors.Hand; lblPublicIP.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); lblPublicIP.ForeColor = Color.Lime; - lblPublicIP.Location = new Point(186, 585); + lblPublicIP.Location = new Point(177, 724); lblPublicIP.Name = "lblPublicIP"; lblPublicIP.Size = new Size(62, 17); lblPublicIP.TabIndex = 19; @@ -421,9 +373,9 @@ private void InitializeComponent() btnRestart.Cursor = Cursors.Hand; btnRestart.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); btnRestart.ForeColor = Color.DarkCyan; - btnRestart.Location = new Point(681, 589); + btnRestart.Location = new Point(681, 726); btnRestart.Name = "btnRestart"; - btnRestart.Size = new Size(101, 28); + btnRestart.Size = new Size(101, 32); btnRestart.TabIndex = 20; btnRestart.Text = "📡 Restart"; btnRestart.UseVisualStyleBackColor = true; @@ -434,12 +386,13 @@ private void InitializeComponent() lblUpdateStatus.BackColor = Color.DodgerBlue; lblUpdateStatus.Font = new Font("Segoe UI", 11.25F, FontStyle.Bold, GraphicsUnit.Point, 0); lblUpdateStatus.ImageAlign = ContentAlignment.MiddleLeft; - lblUpdateStatus.Location = new Point(12, 104); + lblUpdateStatus.Location = new Point(12, 131); lblUpdateStatus.Name = "lblUpdateStatus"; - lblUpdateStatus.Size = new Size(881, 33); + lblUpdateStatus.Size = new Size(881, 37); lblUpdateStatus.TabIndex = 21; lblUpdateStatus.Text = "Version Check Message"; lblUpdateStatus.TextAlign = ContentAlignment.MiddleLeft; + lblUpdateStatus.MouseDown += Form_Drag_MouseDown; // // btnDownloadUpdate // @@ -447,9 +400,9 @@ private void InitializeComponent() btnDownloadUpdate.Cursor = Cursors.Hand; btnDownloadUpdate.FlatStyle = FlatStyle.Popup; btnDownloadUpdate.ImageAlign = ContentAlignment.TopLeft; - btnDownloadUpdate.Location = new Point(710, 110); + btnDownloadUpdate.Location = new Point(711, 137); btnDownloadUpdate.Name = "btnDownloadUpdate"; - btnDownloadUpdate.Size = new Size(161, 22); + btnDownloadUpdate.Size = new Size(161, 25); btnDownloadUpdate.TabIndex = 22; btnDownloadUpdate.Text = "Download"; btnDownloadUpdate.UseVisualStyleBackColor = false; @@ -457,21 +410,134 @@ private void InitializeComponent() // // chkPrivacyMode // - chkPrivacyMode.Location = new Point(427, 589); + chkPrivacyMode.Location = new Point(427, 726); chkPrivacyMode.Name = "chkPrivacyMode"; - chkPrivacyMode.Size = new Size(125, 28); + chkPrivacyMode.Size = new Size(125, 32); chkPrivacyMode.TabIndex = 23; chkPrivacyMode.Text = "Privacy Mode"; chkPrivacyMode.UseVisualStyleBackColor = true; chkPrivacyMode.CheckedChanged += chkPrivacyMode_CheckedChanged; // + // btnClose + // + btnClose.Cursor = Cursors.Hand; + btnClose.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); + btnClose.Location = new Point(1204, 9); + btnClose.Name = "btnClose"; + btnClose.Size = new Size(25, 25); + btnClose.TabIndex = 24; + btnClose.Text = "❌"; + btnClose.UseVisualStyleBackColor = true; + btnClose.Click += btnClose_Click; + // + // btnMinimize + // + btnMinimize.Cursor = Cursors.Hand; + btnMinimize.Font = new Font("Segoe UI", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0); + btnMinimize.Location = new Point(1173, 9); + btnMinimize.Name = "btnMinimize"; + btnMinimize.Size = new Size(25, 25); + btnMinimize.TabIndex = 25; + btnMinimize.Text = "-"; + btnMinimize.UseVisualStyleBackColor = true; + btnMinimize.Click += btnMinimize_Click; + // + // btnDiscord + // + btnDiscord.Cursor = Cursors.Hand; + btnDiscord.Location = new Point(1142, 9); + btnDiscord.Name = "btnDiscord"; + btnDiscord.Size = new Size(25, 25); + btnDiscord.TabIndex = 26; + btnDiscord.Text = "Discord Icon"; + btnDiscord.UseVisualStyleBackColor = true; + btnDiscord.Click += btnDiscord_Click; + // + // btnGithub + // + btnGithub.Cursor = Cursors.Hand; + btnGithub.Location = new Point(1111, 9); + btnGithub.Name = "btnGithub"; + btnGithub.Size = new Size(25, 25); + btnGithub.TabIndex = 27; + btnGithub.Text = "Github"; + btnGithub.UseVisualStyleBackColor = true; + btnGithub.Click += btnGithub_Click; + // + // DisplayIcon + // + DisplayIcon.HeaderText = ""; + DisplayIcon.Name = "DisplayIcon"; + DisplayIcon.ReadOnly = true; + DisplayIcon.Width = 5; + // + // colGame + // + colGame.DataPropertyName = "Game"; + colGame.HeaderText = "Game"; + colGame.Name = "colGame"; + colGame.ReadOnly = true; + colGame.Width = 175; + // + // colName + // + colName.DataPropertyName = "ServerName"; + colName.HeaderText = "Server Name"; + colName.Name = "colName"; + colName.ReadOnly = true; + colName.Width = 265; + // + // colPort + // + colPort.DataPropertyName = "Port"; + colPort.HeaderText = "Port"; + colPort.Name = "colPort"; + colPort.ReadOnly = true; + colPort.Width = 80; + // + // colQueryPort + // + colQueryPort.DataPropertyName = "QueryPort"; + colQueryPort.HeaderText = "Query Port"; + colQueryPort.Name = "colQueryPort"; + colQueryPort.ReadOnly = true; + colQueryPort.Width = 80; + // + // colPlayerCount + // + colPlayerCount.DataPropertyName = "PlayerCount"; + colPlayerCount.HeaderText = "Players"; + colPlayerCount.Name = "colPlayerCount"; + colPlayerCount.ReadOnly = true; + colPlayerCount.Width = 70; + // + // colUptime + // + colUptime.DataPropertyName = "Uptime"; + colUptime.HeaderText = "UPTIME"; + colUptime.Name = "colUptime"; + colUptime.ReadOnly = true; + colUptime.Width = 80; + // + // colStatus + // + colStatus.DataPropertyName = "Status"; + colStatus.HeaderText = "Status"; + colStatus.Name = "colStatus"; + colStatus.ReadOnly = true; + colStatus.Width = 90; + // // MainGUI // - AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleDimensions = new SizeF(7F, 17F); AutoScaleMode = AutoScaleMode.Font; BackgroundImage = Properties.Resources.background; BackgroundImageLayout = ImageLayout.Stretch; - ClientSize = new Size(1241, 628); + ClientSize = new Size(1241, 772); + Controls.Add(btnGithub); + Controls.Add(btnDiscord); + Controls.Add(btnMinimize); + Controls.Add(btnClose); Controls.Add(chkPrivacyMode); Controls.Add(btnDownloadUpdate); Controls.Add(lblUpdateStatus); @@ -487,7 +553,8 @@ private void InitializeComponent() Controls.Add(btnStop); Controls.Add(btnStart); Controls.Add(rtbLog); - FormBorderStyle = FormBorderStyle.FixedSingle; + Font = new Font("Segoe UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point, 0); + FormBorderStyle = FormBorderStyle.None; Icon = (Icon)resources.GetObject("$this.Icon"); MaximizeBox = false; Name = "MainGUI"; @@ -495,6 +562,7 @@ private void InitializeComponent() Text = "Synix Control Panel"; FormClosing += MainForm_FormClosing; Shown += MainGUI_Shown; + MouseDown += Form_Drag_MouseDown; ((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit(); ((System.ComponentModel.ISupportInitialize)logo).EndInit(); ((System.ComponentModel.ISupportInitialize)chartHeartbeat).EndInit(); @@ -543,12 +611,17 @@ private void InitializeComponent() private Button btnDownloadUpdate; private CheckBox chkPrivacyMode; private ToolStripMenuItem btnExportBatch; + private Button btnClose; + private Button btnMinimize; + private Button btnDiscord; + private Button btnGithub; + private DataGridViewTextBoxColumn DisplayIcon; private DataGridViewTextBoxColumn colGame; private DataGridViewTextBoxColumn colName; private DataGridViewTextBoxColumn colPort; private DataGridViewTextBoxColumn colQueryPort; - private DataGridViewTextBoxColumn PlayerCountDisplay; - private DataGridViewTextBoxColumn UptimeDisplay; + private DataGridViewTextBoxColumn colPlayerCount; + private DataGridViewTextBoxColumn colUptime; private DataGridViewTextBoxColumn colStatus; } } diff --git a/MainGUI.cs b/MainGUI.cs index 7619d48..7ad30dc 100644 --- a/MainGUI.cs +++ b/MainGUI.cs @@ -20,6 +20,7 @@ using System.Diagnostics; using System.Windows.Forms.DataVisualization.Charting; using static Synix_Control_Panel.SynixEngine.Core; +using System.Runtime.InteropServices; namespace Synix_Control_Panel { @@ -36,24 +37,67 @@ public partial class MainGUI : Form private static Font boldFont = new Font("Segoe UI", 9, FontStyle.Bold); private static Font regularFont = new Font("Segoe UI", 9, FontStyle.Regular); private bool isPrivacyLoading = false; + private System.Windows.Forms.Timer? versionTimer; + public static Dictionary ServerIconsCache = new Dictionary(); + + public const int WM_NCLBUTTONDOWN = 0xA1; + public const int HT_CAPTION = 0x2; + + [DllImport("user32.dll")] + public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); + [DllImport("user32.dll")] + public static extern bool ReleaseCapture(); public MainGUI() { InitializeComponent(); Instance = this; FileHandler.LoadServers(); - _ = Core.Instance; - GridStyler.DarkTheme(dataGridView1); UIStyleHelper.InitializeToggles(this); + dataGridView1.AutoGenerateColumns = false; dataGridView1.DataSource = serverList; + //dataGridView1.DataError += dataGridView1_DataError; + if (!dataGridView1.Columns.Contains("IconCol")) + { + DataGridViewImageColumn iconCol = new DataGridViewImageColumn(); + iconCol.Name = "IconCol"; + iconCol.HeaderText = ""; + iconCol.DataPropertyName = "DisplayIcon"; + iconCol.ImageLayout = DataGridViewImageCellLayout.Zoom; + iconCol.Width = 35; + iconCol.DefaultCellStyle.Padding = new Padding(6); + + dataGridView1.Columns.Insert(0, iconCol); + dataGridView1.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None; + dataGridView1.RowTemplate.Height = 35; + foreach (DataGridViewRow row in dataGridView1.Rows) + { + row.Height = 35; + } + } + + GridStyler.DarkTheme(dataGridView1); + GridStyler.ApplyRoundedCorners(dataGridView1, 10); typeof(DataGridView).InvokeMember("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, null, dataGridView1, new object[] { true }); GridStyler.ApplyTransparentTheme(dataGridView1); - Instance = this; + GridStyler.StyleCloseButton(btnClose); + GridStyler.StyleMinimizeButton(btnMinimize); + GridStyler.StyleIconButton(btnDiscord, Properties.Resources.discord_icon, Color.FromArgb(88, 101, 242)); + GridStyler.StyleIconButton(btnGithub, Properties.Resources.github_icon, Color.FromArgb(200, 200, 200)); + chkPrivacyMode.Text = "Privacy Mode"; chkPrivacyMode.Checked = Properties.Settings.Default.PrivacyMode; + this.Region = System.Drawing.Region.FromHrgn(CreateRoundRectRgn(0, 0, this.Width, this.Height, 15, 15)); isPrivacyLoading = chkPrivacyMode.Checked; _ = LoadNetworkInfo(); + _ = Core.Instance; _ = VersionCheck(); + InitializeVersionCheckTimer(); + } + + private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e) + { + e.ThrowException = false; } private void tmrResourceUpdates_Tick(object sender, EventArgs e) @@ -91,9 +135,14 @@ private void tmrResourceUpdates_Tick(object sender, EventArgs e) if (needsTimeCheck) { string currentExactTime = DateTime.Now.ToString("HH:mm:ss"); + int currentDayIndex = (int)DateTime.Now.DayOfWeek; + foreach (var server in serverList) { - if (server.IsScheduledRestartEnabled && currentExactTime == (server.RestartTime + ":00")) + if (server.IsScheduledRestartEnabled && + server.RestartDays != null && + server.RestartDays[currentDayIndex] && + currentExactTime == (server.RestartTime + ":00")) { _ = Core.Instance.ExecuteStartSequence(server, "MAINTENANCE"); } @@ -102,6 +151,26 @@ private void tmrResourceUpdates_Tick(object sender, EventArgs e) chartTickCounter++; } + [DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")] + private static extern IntPtr CreateRoundRectRgn + ( + int nLeftRect, // x-coordinate of upper-left corner + int nTopRect, // y-coordinate of upper-left corner + int nRightRect, // x-coordinate of lower-right corner + int nBottomRect, // y-coordinate of lower-right corner + int nWidthEllipse, // width of the rounded corner + int nHeightEllipse // height of the rounded corner + ); + + private void Form_Drag_MouseDown(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + ReleaseCapture(); + SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0); + } + } + private void CheckRunningStatus() { string[] spinFrames = { "|", "/", "--", "\\" }; @@ -138,6 +207,13 @@ private void CheckRunningStatus() int nextIndex = (currentIndex + 1) % spinFrames.Length; server.Status = "Backing Up " + spinFrames[nextIndex]; } + else if (status.StartsWith("Stopping")) + { + string currentFrame = status.Replace("Stopping ", ""); + int currentIndex = Array.IndexOf(spinFrames, currentFrame); + int nextIndex = (currentIndex + 1) % spinFrames.Length; + server.Status = "Stopping " + spinFrames[nextIndex]; + } } UpdateGrid(); } @@ -549,6 +625,22 @@ private void btnHelp_Click(object sender, EventArgs e) } } + private void InitializeVersionCheckTimer() + { + versionTimer = new System.Windows.Forms.Timer(); + + // 20 minutes * 60 seconds * 1000 milliseconds + versionTimer.Interval = 20 * 60 * 1000; + + versionTimer.Tick += async (sender, e) => + { + // This fires every 20 minutes in the background + await VersionCheck(); + }; + + versionTimer.Start(); + } + private async Task VersionCheck() { string currentVersion = "Unknown"; @@ -674,5 +766,41 @@ private void btnExportBatch_Click(object sender, EventArgs e) "Export Complete", MessageBoxButtons.OK, MessageBoxIcon.Information); } } + + private void btnClose_Click(object sender, EventArgs e) + { + Application.Exit(); + } + + private void btnMinimize_Click(object sender, EventArgs e) + { + this.WindowState = FormWindowState.Minimized; + } + + private void btnDiscord_Click(object sender, EventArgs e) + { + OpenUrl("https://discord.gg/WduKEU3j8s"); + } + + private void btnGithub_Click(object sender, EventArgs e) + { + OpenUrl("https://github.com/ubidzz/Synix-Control-Panel"); + } + + private void OpenUrl(string url) + { + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + catch (Exception ex) + { + MessageBox.Show($"Unable to open the link automatically.\n\nError: {ex.Message}", "Link Error", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } } } diff --git a/MainGUI.resx b/MainGUI.resx index 8dce2c9..e46bbc9 100644 --- a/MainGUI.resx +++ b/MainGUI.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + True + True @@ -129,10 +132,10 @@ True - + True - + True diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index 0bcc161..360e8a3 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -70,6 +70,26 @@ internal static System.Drawing.Bitmap background { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap discord_icon { + get { + object obj = ResourceManager.GetObject("discord_icon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap github_icon { + get { + object obj = ResourceManager.GetObject("github_icon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/Properties/Resources.resx b/Properties/Resources.resx index 25cd251..f38c0ae 100644 --- a/Properties/Resources.resx +++ b/Properties/Resources.resx @@ -121,6 +121,12 @@ ..\Images\background.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Images\discord.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Images\github.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Images\logo.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index cef0111..e9c309c 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Synix_Control_Panel.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "18.5.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "18.6.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/README.md b/README.md index a3ef076..b374b1d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![VirusTotal](https://img.shields.io/badge/VirusTotal-1%2F71%20Clean-yellowgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/c3a62c98e52bacccb57bc4e9b342feef20d2be49de4f91bfca164f7e6487d0b8?nocache=1) [![WinGet Status](https://img.shields.io/winget/v/ubidzz.Synix?style=for-the-badge&color=blue&label=WINGET%20INSTALL)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/u/ubidzz/Synix) [![Donate with PayPal](https://img.shields.io/badge/PAYPAL-DONATE-0079C1?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/donate/?hosted_button_id=FAHU6EH6BX9J8) +![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/ubidzz/Synix-Control-Panel/total?style=for-the-badge&logo=github) **Synix Control Panel** is an elite, engine-driven management suite designed to provide a centralized "Brain" for game server hosting. By moving beyond simple batch scripts, Synix automates deployment, process health, networking diagnostics, and hardware stewardship within a **Zero-Admin (No UAC)** environment. diff --git a/ServerHandler/GameServer.cs b/ServerHandler/GameServer.cs index c185733..8a93d2b 100644 --- a/ServerHandler/GameServer.cs +++ b/ServerHandler/GameServer.cs @@ -21,6 +21,8 @@ public class GameInfo { public string Game { get; init; } = string.Empty; [JsonIgnore] + public System.Drawing.Image DisplayIcon { get; set; } + [JsonIgnore] public bool HasAnnouncedOnline { get; set; } = false; [JsonIgnore] public bool NeedsConfigWarning { get; internal set; } @@ -80,7 +82,7 @@ public class GameServer : GameInfo public bool IsFirstBoot { get; set; } = true; public string WorldSeed { get; set; } = "12345"; [JsonIgnore] - public string PlayerCountDisplay => $"{CurrentPlayers} / {MaxPlayers}"; + public string PlayerCount => $"{CurrentPlayers} / {MaxPlayers}"; public int? AppPort { get; set; } = 10777; public bool UpdateOnStart { get; set; } = false; public bool BackupOnStart { get; set; } = false; @@ -91,7 +93,7 @@ public class GameServer : GameInfo public bool IsProbing { get; set; } = false; [JsonIgnore] - public string UptimeDisplay + public string Uptime { get { diff --git a/ServerHandler/ServerSettingsGUI.cs b/ServerHandler/ServerSettingsGUI.cs index 7bdde59..e6991d5 100644 --- a/ServerHandler/ServerSettingsGUI.cs +++ b/ServerHandler/ServerSettingsGUI.cs @@ -336,6 +336,14 @@ private void btnSave_Click(object sender, EventArgs e) if (!Core.Instance.ValidatePortsAndReport(_existingServer, gPort, qPort, rPort, chkEnableRcon.Checked, aPort ?? 0, numAppPort.Enabled, selectedGame)) return; string newPath = txtInstallPath.Text.Trim(); NewServer = new GameServer { Game = selectedGame, ServerName = newName, Port = gPort, QueryPort = qPort, RconPort = rPort, AppPort = aPort, Password = txtPassword.Text, AdminPassword = txtAdminPassword.Text, MaxPlayers = (int)numMaxPlayers.Value, WorldName = cmbWorldName.Text, GameMode = cmbCompetitive.Text, WorldSeed = txtWorldSeed.Text.Trim(), ExtraArgs = txtExtraArgs.Text, IsDefaultPath = chkDefaultPath.Checked, UpdateOnStart = chkUpdateOnStart.Checked, EnableRcon = chkEnableRcon.Checked, RconPassword = txtRconPassword.Text, InstallPath = newPath, IsScheduledRestartEnabled = chkEnableSchedule.Checked, RestartTime = _selectedTime, RestartDays = (bool[])_selectedDays.Clone(), IsDiscordAlertEnabled = chkEnableDiscord.Checked, DiscordWebhook = txtDiscordWebhook.Text.Trim(), Status = _existingServer?.Status ?? StatusManager.GetStatus(ServerState.Stopped), BackupOnStart = chkBackupOnStart.Checked }; + + if (!IsGameServerConfigSafe(NewServer)) + { + MessageBox.Show("Security Alert: One of your inputs contains illegal characters.", + "Input Blocked", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + try { if (_isEditMode && _existingServer != null) @@ -348,6 +356,22 @@ private void btnSave_Click(object sender, EventArgs e) } } else MainGUI.serverList.Add(NewServer); + + var masterData = GameDatabase.GetGame(NewServer.Game); + if (masterData != null) + { + NewServer.AppID = masterData.AppID; + NewServer.ExeName = masterData.ExeName; + + string fullExePath = System.IO.Path.Combine(NewServer.InstallPath, NewServer.ExeName); + string iconPath = Synix_Control_Panel.SynixEngine.Core.GetLocalServerIcon(NewServer.AppID, fullExePath); + + if (System.IO.File.Exists(iconPath)) + { + NewServer.DisplayIcon = System.Drawing.Image.FromFile(iconPath); + } + } + FileHandler.SaveServers(); this.DialogResult = DialogResult.OK; this.Close(); } catch (Exception ex) { MessageBox.Show(ex.Message); } diff --git a/ServerHandler/Servers.cs b/ServerHandler/Servers.cs index 5a68ada..d9eb80b 100644 --- a/ServerHandler/Servers.cs +++ b/ServerHandler/Servers.cs @@ -12,9 +12,10 @@ // ============================================================================ using Synix_Control_Panel.Database; using Synix_Control_Panel.SynixEngine; +using static Synix_Control_Panel.SynixEngine.Core; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Runtime.InteropServices; -using static Synix_Control_Panel.SynixEngine.Core; namespace Synix_Control_Panel.ServerHandler { @@ -39,9 +40,12 @@ public static class Servers public static async Task Start(GameServer server, Action logCallback, StartContext context = StartContext.Manual) { - if (!IsSystemSafeToStart()) return; try { + // 1. HARDWARE CHECKS (Backgrounded to prevent WMI/PerfCounter UI Freezes) + bool isSystemSafe = await Task.Run(() => IsSystemSafeToStart()); + if (!isSystemSafe) return; + if (!Core.Instance.PassResourceGuard(out string guardMsg)) { logCallback?.Invoke(guardMsg, Color.Orange); @@ -49,7 +53,6 @@ public static async Task Start(GameServer server, Action logCallb return; } - // 1. PRE-FLIGHT (Backup & Update) if (server.BackupOnStart && context != StartContext.CrashRecovery) { await Task.Run(() => Core.Instance.ExecuteBackup(server, context)); @@ -60,148 +63,167 @@ public static async Task Start(GameServer server, Action logCallb await Task.Run(() => Core.Instance.UpdateServerAndReport(server, "UPDATE", true)); } - // 2. TEMPLATE VALIDATION + // Safely update the DataGridView UI state on the main thread server.Status = StatusManager.GetStatus(ServerState.Starting); - var dbEntry = GameDatabase.GetGame(server.Game); - if (dbEntry == null) - { - logCallback?.Invoke("[🚨 ERROR] Game template not found.", Color.Red); - return; - } + MainGUI.Instance?.Invoke((Action)(() => MainGUI.Instance.UpdateGrid())); - // 3. PATH SETUP - string fullExePath = Path.Combine(server.InstallPath, dbEntry.ExeName); - string binDir = Path.GetDirectoryName(fullExePath) ?? ""; + ProcessStartInfo? psi = null; + string finalArgs = ""; - if (!File.Exists(fullExePath)) + // 2. HEAVY DISK & STRING PROCESSING (Backgrounded to prevent lag) + await Task.Run(() => { - logCallback?.Invoke($"[🚨 ERROR] Executable missing: {fullExePath}", Color.Red); - server.Status = StatusManager.GetStatus(ServerState.Stopped); - return; - } + var dbEntry = GameDatabase.GetGame(server.Game); + if (dbEntry == null) + { + logCallback?.Invoke("[🚨 ERROR] Game template not found.", Color.Red); + return; + } - // 4. DYNAMIC IDENTITY & SEARCH - string targetId = dbEntry.AppID; - string invokedId = targetId; + string fullExePath = Path.Combine(server.InstallPath, dbEntry.ExeName); + string binDir = Path.GetDirectoryName(fullExePath) ?? ""; - string appidPath = ""; + if (!File.Exists(fullExePath)) + { + logCallback?.Invoke($"[🚨 ERROR] Executable missing: {fullExePath}", Color.Red); + MainGUI.Instance?.Invoke((Action)(() => server.Status = StatusManager.GetStatus(ServerState.Stopped))); + return; + } - try - { - // This creates a "scanner" that looks through every single subfolder - var scanner = Directory.EnumerateFiles(server.InstallPath, "steam_appid.txt", new EnumerationOptions + string targetId = dbEntry.AppID; + string invokedId = targetId; + + string rootAppIdPath = Path.Combine(server.InstallPath, "steam_appid.txt"); + string binAppIdPath = Path.Combine(binDir, "steam_appid.txt"); + string appidPath = rootAppIdPath; + + if (File.Exists(rootAppIdPath)) + { + appidPath = rootAppIdPath; + } + else if (File.Exists(binAppIdPath)) + { + appidPath = binAppIdPath; + } + else { - // Keep looking through every subfolder - RecurseSubdirectories = true, + try + { + var scanner = Directory.EnumerateFiles(server.InstallPath, "steam_appid.txt", new EnumerationOptions + { + RecurseSubdirectories = true, + IgnoreInaccessible = true, + MaxRecursionDepth = 5, + AttributesToSkip = FileAttributes.ReparsePoint + }); + + appidPath = scanner.FirstOrDefault() ?? rootAppIdPath; + } + catch + { + appidPath = rootAppIdPath; + } + } - // If it hits a folder it can't open (locked/protected), skip it and keep going - IgnoreInaccessible = true, + if (File.Exists(appidPath)) + { + try + { + string fileContent = File.ReadAllText(appidPath).Trim(); - // Use the maximum possible depth (effectively unlimited) - MaxRecursionDepth = int.MaxValue, + fileContent = fileContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim() ?? ""; - // Skip things like symlinks to avoid getting stuck in a loop - AttributesToSkip = FileAttributes.ReparsePoint - }); + if (!string.IsNullOrWhiteSpace(fileContent)) + { + invokedId = fileContent; + } + } + catch (Exception ex) { logCallback?.Invoke($"[⚠️ WARNING] File Read Error: {ex.Message}", Color.OrangeRed); } + } - // Find the first one that exists - appidPath = scanner.FirstOrDefault(); - } - catch - { - // If something goes catastrophic, fallback to the root - appidPath = Path.Combine(server.InstallPath, "steam_appid.txt"); - } + string cleanIdentity = Core.Instance.GetSafeName(server.ServerName); + + string args = dbEntry.RequiredArgs + .Replace("{app_port}", server.AppPort?.ToString() ?? "0") + .Replace("{seed}", string.IsNullOrWhiteSpace(server.WorldSeed) ? "12345" : server.WorldSeed) + .Replace("{map}", server.WorldName) + .Replace("{steamAppID}", invokedId) + .Replace("{appid}", targetId) + .Replace("{port}", server.Port.ToString()) + .Replace("{query}", server.QueryPort.ToString()) + .Replace("{MaxPlayers}", server.MaxPlayers.ToString()) + .Replace("{pass}", server.Password ?? "") + .Replace("{adminpass}", server.AdminPassword ?? "") + .Replace("{ServerName}", server.ServerName) + .Replace("{InstallPath}", server.InstallPath) + .Replace("{Identity}", cleanIdentity); + + if (args.Contains("{rcon}")) + { + string formattedRcon = server.EnableRcon && !string.IsNullOrWhiteSpace(dbEntry.RconSyntax) + ? dbEntry.RconSyntax.Replace("{rcon_port}", server.RconPort.ToString()).Replace("{rcon_pass}", server.RconPassword ?? "") + : ""; + args = args.Replace("{rcon}", formattedRcon); + } - // If it's still empty, it truly isn't in that install folder - if (string.IsNullOrEmpty(appidPath)) - { - appidPath = Path.Combine(server.InstallPath, "steam_appid.txt"); - } + if (args.Contains("{mode}") && !string.IsNullOrWhiteSpace(server.GameMode)) + { + string translatedMode = (server.GameMode == "PVE" && (server.Game.Contains("ARK") || server.Game == "Atlas" || server.Game == "Rust")) + ? "True" : (server.GameMode == "PVP" && (server.Game.Contains("ARK") || server.Game == "Atlas" || server.Game == "Rust")) + ? "False" : server.GameMode; + args = args.Replace("{mode}", translatedMode); + } - // 🎯 THE INVOKE: Pull the ID from the file for {steamAppID} - if (File.Exists(appidPath)) - { - try + if (!string.IsNullOrWhiteSpace(server.ExtraArgs)) { - string fileContent = File.ReadAllText(appidPath).Trim(); - if (!string.IsNullOrWhiteSpace(fileContent)) + if (!IsGameServerConfigSafe(server.ExtraArgs)) { - invokedId = fileContent; + logCallback?.Invoke("[🚨 SECURITY] Illegal characters detected in the extra arguments. Aborting startup.", Color.Red); + MainGUI.Instance?.Invoke((Action)(() => server.Status = StatusManager.GetStatus(ServerState.Stopped))); + return; } - } - catch (Exception ex) { logCallback?.Invoke($"[⚠️ WARNING] File Read Error: {ex.Message}", Color.OrangeRed); } - } - // 🛠️ 6. ARGUMENT REPLACEMENT - string cleanIdentity = Core.Instance.GetSafeName(server.ServerName); - - string args = dbEntry.RequiredArgs - .Replace("{app_port}", server.AppPort?.ToString() ?? "0") - .Replace("{seed}", string.IsNullOrWhiteSpace(server.WorldSeed) ? "12345" : server.WorldSeed) - .Replace("{map}", server.WorldName) - .Replace("{steamAppID}", invokedId) - .Replace("{appid}", targetId) - .Replace("{port}", server.Port.ToString()) - .Replace("{query}", server.QueryPort.ToString()) - .Replace("{MaxPlayers}", server.MaxPlayers.ToString()) - .Replace("{pass}", server.Password ?? "") - .Replace("{adminpass}", server.AdminPassword ?? "") - .Replace("{ServerName}", server.ServerName) - .Replace("{InstallPath}", server.InstallPath) - .Replace("{Identity}", cleanIdentity); - - // 🎯 RCON LOGIC RESTORED - if (args.Contains("{rcon}")) - { - string formattedRcon = server.EnableRcon && !string.IsNullOrWhiteSpace(dbEntry.RconSyntax) - ? dbEntry.RconSyntax.Replace("{rcon_port}", server.RconPort.ToString()).Replace("{rcon_pass}", server.RconPassword ?? "") - : ""; - args = args.Replace("{rcon}", formattedRcon); - } + args = $"{args} \"{server.ExtraArgs.Trim()}\""; + } - // 🎯 GAME MODE TRANSLATION RESTORED - if (args.Contains("{mode}") && !string.IsNullOrWhiteSpace(server.GameMode)) - { - string translatedMode = (server.GameMode == "PVE" && (server.Game.Contains("ARK") || server.Game == "Atlas" || server.Game == "Rust")) - ? "True" : (server.GameMode == "PVP" && (server.Game.Contains("ARK") || server.Game == "Atlas" || server.Game == "Rust")) - ? "False" : server.GameMode; - args = args.Replace("{mode}", translatedMode); - } + args = args.Replace(" ", " ").Trim(); - if(!string.IsNullOrWhiteSpace(server.ExtraArgs)) - { - args = args + " " + server.ExtraArgs; - } + if (!IsStringSafe(args)) + { + logCallback?.Invoke("[🚨 SECURITY] Illegal characters detected. Aborting startup.", Color.Red); + MainGUI.Instance?.Invoke((Action)(() => server.Status = StatusManager.GetStatus(ServerState.Stopped))); + return; + } - args = args.Replace(" ", " ").Trim(); + // Package the final validated strings into process parameters + finalArgs = args; + psi = new ProcessStartInfo + { + FileName = fullExePath, + Arguments = finalArgs, + WorkingDirectory = binDir, + UseShellExecute = false, + CreateNoWindow = false + }; - // 🚀 7. CONFIGURE PROCESS - ProcessStartInfo psi = new() - { - FileName = fullExePath, - Arguments = args, - WorkingDirectory = binDir, - UseShellExecute = false, - CreateNoWindow = false - }; + psi.EnvironmentVariables["SteamAppId"] = invokedId; + psi.EnvironmentVariables["SteamGameId"] = invokedId; + }); - // 🎯 MEMORY INJECTION - psi.EnvironmentVariables["SteamAppId"] = invokedId; - psi.EnvironmentVariables["SteamGameId"] = invokedId; + // If the background task failed early (missing exe, bad string), safely stop execution + if (psi == null) return; - logCallback?.Invoke($"[ARGUMENT] {args}", Color.Cyan); + // 3. LAUNCH PROCESS (Back on the UI thread, instantaneous) + logCallback?.Invoke($"[ARGUMENT] {finalArgs}", Color.Cyan); - // 🚀 8. EXECUTION & MONITORING Process? proc = Process.Start(psi); if (proc != null) { server.RunningProcess = proc; server.PID = proc.Id; - if (server.StartTime == null) server.StartTime = DateTime.Now; + server.StartTime = DateTime.Now; - // 🎯 DISCORD ALERT: Server Online (Clean alert) _ = Core.Instance.SendDiscordAlert(server, "SERVER STARTING", $"{server.ServerName} process has been initiated.", Color.Cyan); proc.EnableRaisingEvents = true; @@ -209,7 +231,6 @@ public static async Task Start(GameServer server, Action logCallb { if (server.Status == StatusManager.GetStatus(ServerState.Running)) { - // Watchdog handles the single Discord crash notification await Core.Instance.ExecuteStartSequence(server, "WATCHDOG"); } else @@ -237,7 +258,6 @@ public static async Task Stop(GameServer server, Action logCallba return; } - // 🎯 DISCORD ALERT: Manual Shutdown if (isManual) { _ = Core.Instance.SendDiscordAlert(server, "MANUAL SHUTDOWN", @@ -258,7 +278,7 @@ public static async Task Stop(GameServer server, Action logCallba if (cleanExit) { - logCallback?.Invoke($"[STOP] {server.ServerName} saved and closed cleanly.", Color.Lime); + logCallback?.Invoke($"[SYNIX] {server.ServerName} saved and closed cleanly.", Color.Lime); FinalizeStoppedState(server); return; } diff --git a/SynixEngine/Actions.cs b/SynixEngine/Actions.cs index eb5ea12..331638f 100644 --- a/SynixEngine/Actions.cs +++ b/SynixEngine/Actions.cs @@ -21,6 +21,8 @@ namespace Synix_Control_Panel.SynixEngine { public partial class Core { + private static readonly HashSet _activeSequences = new HashSet(); + public async Task StopServerAndReport(GameServer server, bool isManual = true) { server.Status = StatusManager.GetStatus(ServerState.Stopping); @@ -372,70 +374,86 @@ public void EditServerAndReport(GameServer server) public async Task ExecuteStartSequence(GameServer server, string status = "") { - bool stopServer = false; - StartContext currentContext = StartContext.Manual; - - if (!PassResourceGuard(out string guardMsg)) + lock (_activeSequences) { - Log(guardMsg, System.Drawing.Color.Red, true); - MessageBox.Show(guardMsg, "System Resource Exhaustion", - System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Warning); - return; + if (_activeSequences.Contains(server.ServerName)) + { + return; + } + _activeSequences.Add(server.ServerName); } - if (!ValidateIntegrityAndReport(server)) return; - if (ShouldBlockForConfig(server)) return; - - if (status == "RESTART") - { - Log($"[SYNIX] Starting restart sequence for {server.ServerName}...", Color.Cyan); - stopServer = true; - } - else if (status == "MAINTENANCE") - { - Log($"[🛠 MAINTENANCE] Scheduled restart sequence for {server.ServerName}.", Color.Cyan, true); - stopServer = true; - currentContext = StartContext.Scheduled; - } - else if (status == "WATCHDOG") + try { - server.Status = StatusManager.GetStatus(ServerState.Crashed); - string reason = !server.RunningProcess?.Responding ?? false ? "FREEZE" : "CRASH/CLOSE"; - Log($"[🛡️ WATCHDOG] {reason} detected on {server.ServerName}. Initializing recovery...", Color.Orange); + bool stopServer = false; + StartContext currentContext = StartContext.Manual; - _ = SendDiscordAlert(server, "🚨 CRASH DETECTED", - $"{server.ServerName} has terminated. Synix is attempting an automatic restart.", - Color.Red); + if (!PassResourceGuard(out string guardMsg)) + { + Log(guardMsg, System.Drawing.Color.Red, true); + MessageBox.Show(guardMsg, "System Resource Exhaustion", + System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Warning); + return; + } - stopServer = true; - currentContext = StartContext.CrashRecovery; - Core.Instance.UpdateGridStatus(); - } + if (!ValidateIntegrityAndReport(server)) return; + if (ShouldBlockForConfig(server)) return; - if (stopServer) - { - Log($"[SYNIX] Stoping the {server.ServerName} server.", Color.Cyan, true); + if (status == "RESTART") + { + Log($"[SYNIX] Starting restart sequence for {server.ServerName}...", Color.Cyan); + stopServer = true; + } + else if (status == "MAINTENANCE") + { + Log($"[🛠 MAINTENANCE] Scheduled restart sequence for {server.ServerName}.", Color.Cyan, true); + stopServer = true; + currentContext = StartContext.Scheduled; + } + else if (status == "WATCHDOG") + { + server.Status = StatusManager.GetStatus(ServerState.Crashed); + string reason = !server.RunningProcess?.Responding ?? false ? "FREEZE" : "CRASH/CLOSE"; + Log($"[🛡️ WATCHDOG] {reason} detected on {server.ServerName}. Initializing recovery...", Color.Orange); - await StopServerAndReport(server); - } + _ = SendDiscordAlert(server, "🚨 CRASH DETECTED", + $"{server.ServerName} has terminated. Synix is attempting an automatic restart.", + Color.Red); - await Task.Delay(4000); + stopServer = true; + currentContext = StartContext.CrashRecovery; + Core.Instance.UpdateGridStatus(); + } - if (server.Status == StatusManager.GetStatus(ServerState.Stopped)) - { - Log($"[SYNIX] Starting the {server.ServerName} server.", Color.Cyan, true); - if (!PassSpamLock(server, out string lockMsg, "Start")) { Log(lockMsg, System.Drawing.Color.Orange); return; } + if (stopServer && server.PID != null) + { + Log($"[SYNIX] Stoping the {server.ServerName} server.", Color.Cyan, true); + await StopServerAndReport(server); + } - await Servers.Start(server, (msg, Color) => MainGUI.Instance?.Invoke((Action)(() => Log(msg, Color))), currentContext); + if (server.Status == StatusManager.GetStatus(ServerState.Stopped)) + { + Log($"[SYNIX] Starting the {server.ServerName} server.", Color.Cyan, true); + if (!PassSpamLock(server, out string lockMsg, "Start")) { Log(lockMsg, System.Drawing.Color.Orange); return; } + + await Servers.Start(server, (msg, Color) => MainGUI.Instance?.Invoke((Action)(() => Log(msg, Color))), currentContext); + } + else + { + if (server.Status != StatusManager.GetStatus(ServerState.Starting)) + { + Log($"[🚨 CRITICAL] Restart failed: {server.ServerName} is still stuck!", Color.Red); + } + } + stopServer = false; } - else + finally { - if (server.Status != StatusManager.GetStatus(ServerState.Starting)) + lock (_activeSequences) { - Log($"[🚨 CRITICAL] Restart failed: {server.ServerName} is still stuck!", Color.Red); + _activeSequences.Remove(server.ServerName); } } - stopServer = false; } public void RunUniversalHealthCheck() diff --git a/SynixEngine/IconManager.cs b/SynixEngine/IconManager.cs new file mode 100644 index 0000000..45f34b5 --- /dev/null +++ b/SynixEngine/IconManager.cs @@ -0,0 +1,74 @@ +// ============================================================================ +// PROJECT: Synix Game Server Control Panel +// AUTHOR: Jason Turner (ubidzz) +// COPYRIGHT: © 2026 All Rights Reserved. +// +// LEGAL NOTICE: +// This source code is proprietary and confidential. +// 1. Permission is granted for PERSONAL, NON-COMMERCIAL use only. +// 2. You may modify this code for your own use, but you may NOT redistribute, +// rebrand, or sell this code or derivative works without written consent. +// 3. The "Synix" brand and logic remain the property of Jason Turner. +// ============================================================================ +using Synix_Control_Panel.FileFolderHandler; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; + +namespace Synix_Control_Panel.SynixEngine +{ + public partial class Core + { + private static Dictionary _iconPathCache = new Dictionary(); + private const string SynixRoot = @"C:\Synix\SynixData"; + + public static string GetLocalServerIcon(string Appid, string serverPath) + { + // 1. Check in-memory session cache first + if (_iconPathCache.TryGetValue(Appid, out string memoryPath)) + { + return memoryPath; + } + + // 2. Setup the output path in C:\Synix\GameIcons + string iconFolder = Path.Combine(SynixRoot, "GameIcons"); + FolderHandler.Create(iconFolder); + string localIconPath = Path.Combine(iconFolder, $"{Appid}.png"); + + // 3. If already extracted in a past session, return it + if (File.Exists(localIconPath)) + { + _iconPathCache[Appid] = localIconPath; + return localIconPath; + } + + // 4. Extract directly using the full path to the executable + if (File.Exists(serverPath)) + { + try + { + using (Icon extractedIcon = Icon.ExtractAssociatedIcon(serverPath)) + { + if (extractedIcon != null) + { + using (Bitmap bitmap = extractedIcon.ToBitmap()) + { + bitmap.Save(localIconPath, System.Drawing.Imaging.ImageFormat.Png); + _iconPathCache[Appid] = localIconPath; + return localIconPath; + } + } + } + } + catch + { + // Fall through if file is locked, in use, or lacks permissions + } + } + + // 5. Hard Fallback if file doesn't exist or extraction fails + return Path.Combine(SynixRoot, "GameIcons", "default_server.png"); + } + } +} \ No newline at end of file diff --git a/SynixEngine/Status.cs b/SynixEngine/Status.cs index 5088a59..bf99ad1 100644 --- a/SynixEngine/Status.cs +++ b/SynixEngine/Status.cs @@ -175,7 +175,7 @@ public async Task UpdatePlayerCount(GameServer server) { if (server.Status != StatusManager.GetStatus(ServerState.Running)) return; - // 🎯 Use your dynamic LAN IP and Loopback + // 🎯 Use dynamic LAN IP and Loopback string localIp = await Core.Instance.GetLocalIP(); var targets = new List { "127.0.0.1", localIp }.Where(x => !string.IsNullOrEmpty(x)).Distinct(); diff --git a/SynixEngine/Validator.cs b/SynixEngine/Validator.cs index 171323e..6032fcf 100644 --- a/SynixEngine/Validator.cs +++ b/SynixEngine/Validator.cs @@ -11,11 +11,15 @@ // 3. The "Synix" brand and logic remain the property of Jason Turner. // ============================================================================ using Synix_Control_Panel.Database; +using System.Reflection; +using System.Text.RegularExpressions; namespace Synix_Control_Panel.SynixEngine { public partial class Core { + private static readonly Regex SafeRegex = new Regex(@"^[a-zA-Z0-9\s\-+:\""\\/._=?,]*$", RegexOptions.Compiled); + public bool CanServerStart(GameServer server, out string errorMessage) { var dbEntry = GameDatabase.GetGame(server.Game); @@ -243,5 +247,48 @@ public bool PassResourceGuard(out string message) return true; } + // 1. The dedicated string checker (Used by Start sequence) + public static bool IsStringSafe(string input) + { + // If it's empty, it's safe (no injection possible) + if (string.IsNullOrWhiteSpace(input)) return true; + + // Block directory traversal climbing + if (input.Contains("..")) return false; + + // Run the regex (Make sure SafeRegex includes = ? , if you need them for games like ARK/Rust) + return SafeRegex.IsMatch(input); + } + + // 2. The object checker (Used by Save button) + public static bool IsGameServerConfigSafe(object obj) + { + if (obj == null) return false; + + // SAFETY CATCH: If someone accidentally passes a direct string into the object checker, + // route it to the string checker instead of using Reflection. + if (obj is string directString) + { + return IsStringSafe(directString); + } + + PropertyInfo[] properties = obj.GetType().GetProperties(); + + foreach (var prop in properties) + { + if (prop.PropertyType == typeof(string)) + { + string value = (string)prop.GetValue(obj); + + // Pass the extracted string to our dedicated checker + if (!IsStringSafe(value)) + { + Core.Instance.Log($"[🚨 SECURITY] Illegal characters found in property: {prop.Name}"); + return false; + } + } + } + return true; + } } } \ No newline at end of file diff --git a/SynixEngine/version.txt b/SynixEngine/version.txt index d0f65cd..835ea23 100644 --- a/SynixEngine/version.txt +++ b/SynixEngine/version.txt @@ -1 +1 @@ -1.0.16 \ No newline at end of file +1.0.17 \ No newline at end of file