sofii/SoFii/MainWindow.cs
2023-10-08 02:10:21 +02:00

468 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace SoFii {
public partial class MainWindow : Form {
private bool IsSizing = false;
private readonly ColourCharInfo[] Colours = EmbeddedResources.GetColourChars();
public MainWindow() {
InitializeComponent();
ResizeToFit(false);
Screen screen = Screen.FromControl(this);
Rectangle screenRect = screen.WorkingArea;
int left = (screenRect.Width / 2) - (Size.Width / 2);
int top = (screenRect.Height / 4) - (Size.Height / 2);
Location = new Point(screenRect.Y + left, screenRect.X + top);
versionLabel.Text = $"SoFii v{Program.GetSemVerString()}";
serverAddressInput.Text = Settings.ServerAddress;
if(Settings.UseDefaultServer) {
serverAddressInput.Enabled = false;
serverUseCustom.Checked = false;
LoadDefaultServer();
} else {
serverAddressInput.Enabled = true;
serverUseCustom.Checked = true;
}
CheckGameExists(Settings.GamePath);
DrawPlayerNameColourButtons();
playerNameInput.Text = Settings.PlayerName;
DrawPlayerNamePreview();
gfxResolutionSelect.Items.AddRange(new object[] {
string.Empty,
new SoF2VideoMode(3, "640x480"),
new SoF2VideoMode(4, "800x600"),
new SoF2VideoMode(5, "960x720"),
new SoF2VideoMode(6, "1024x768"),
new SoF2VideoMode(7, "1152x864"),
new SoF2VideoMode(8, "1280x1024"),
new SoF2VideoMode(9, "1600x1200"),
new SoF2VideoMode(10, "2048x1536"),
});
int gfxMode = Settings.GfxMode;
if(gfxMode >= 0)
gfxResolutionSelect.SelectedItem = gfxMode;
gfxFullscreen.Checked = Settings.GfxFullscreen;
}
private void DrawPlayerNameColourButtons() {
Control.ControlCollection container = playerNameColoursBox.Controls;
Button template = playerNameColourButton;
container.Remove(template);
int width = template.Size.Width;
int height = template.Size.Height;
int rowStart = template.Location.Y;
int colStart = template.Location.X;
int tabIndex = template.TabIndex;
int row = 0;
int col = 0;
List<int> handled = new List<int>();
foreach(ColourCharInfo cci in Colours) {
int argb = cci.Colour.ToArgb();
if(handled.Contains(argb))
continue;
handled.Add(argb);
Button colourButton = new Button() {
Text = cci.Char.ToString(),
TextAlign = ContentAlignment.BottomRight,
FlatStyle = FlatStyle.Flat,
UseVisualStyleBackColor = false,
BackColor = cci.Colour,
TabIndex = tabIndex,
Size = new Size(width, height),
Location = new Point(colStart + (col * width) + (col * 2), rowStart + (row * height) + (row * 2)),
};
colourButton.Click += (s, e) => {
int selPos = playerNameInput.SelectionStart;
playerNameInput.Text = playerNameInput.Text.Insert(selPos, $"^{cci.Char}");
playerNameInput.SelectionStart = selPos + 2;
};
container.Add(colourButton);
++tabIndex;
if(++col > 11) {
col = 0;
++row;
}
}
}
public void ResizeToFit(bool animate = true) {
int needHeight = 0;
Point scrollPos = AutoScrollPosition;
AutoScrollPosition = new Point(0, 0);
foreach(Control control in mainTabs.SelectedTab.Controls) {
int calcHeight = control.Size.Height + control.Location.Y;
if(calcHeight > needHeight)
needHeight = calcHeight;
}
needHeight += 83;
Screen screen = Screen.FromControl(this);
int maxHeight = (screen.WorkingArea.Height / 8) * 5;
if(needHeight > maxHeight)
needHeight = maxHeight;
void onFinish() {
AutoScrollPosition = scrollPos;
};
if(animate)
SetHeightAnimated(needHeight, onFinish);
else {
ClientSize = new Size(ClientSize.Width, needHeight);
onFinish();
}
}
public void SetHeightAnimated(int height, Action onFinish = null) {
if(IsSizing)
return;
IsSizing = true;
const int timeout = 1000 / 60;
double time = 0;
double period = timeout / 150d;
int currentHeight = ClientSize.Height;
int diffHeight = height - currentHeight;
Action setHeight = new Action(() => {
int newHeight = currentHeight + (int)Math.Ceiling(time * diffHeight);
ClientSize = new Size(ClientSize.Width, newHeight);
});
new Thread(() => {
Stopwatch sw = new Stopwatch();
try {
do {
sw.Reset();
sw.Start();
Invoke(setHeight);
time += period;
int delay = timeout - (int)sw.ElapsedMilliseconds;
if(delay > 1)
Thread.Sleep(delay);
} while(time < 1d);
} finally {
sw.Stop();
InvokeAction(() => {
ClientSize = new Size(ClientSize.Width, height);
onFinish?.Invoke();
Refresh();
});
IsSizing = false;
}
}) {
IsBackground = true,
Priority = ThreadPriority.AboveNormal,
}.Start();
}
public void InvokeAction(Action action) {
if(action is null)
throw new ArgumentNullException(nameof(action));
if(InvokeRequired)
Invoke(action);
else
action();
}
public bool CheckGameExists(string path) {
bool exists = !string.IsNullOrEmpty(path) && File.Exists(path);
gameLocationInput.Text = exists ? path : "Click the Browse... button and pick SoF2MP.exe";
launchGameButton.Enabled = exists;
return exists;
}
public void LaunchGame() {
string path = Settings.GamePath;
if(!CheckGameExists(path)) {
MessageBox.Show(this, "Game executable doesn't exist anymore. Please use the Browse... button to select it again.", "SoFii :: Error while launching game");
return;
}
string serverAddr = Settings.ServerAddress;
if(!Settings.UseDefaultServer && !serverAddressInput.Text.Equals(serverAddr))
Settings.ServerAddress = serverAddr = serverAddressInput.Text;
try {
Enabled = false;
WindowState = FormWindowState.Minimized;
StringBuilder args = new StringBuilder();
args.AppendFormat(@"+seta r_fullscreen ""{0}"" ", Settings.GfxFullscreen ? '1' : '0');
int vidMode = Settings.GfxMode;
if(vidMode >= 0)
args.AppendFormat(@"+seta r_mode ""{0}"" ", vidMode);
args.Append("+vid_restart ");
string playerName = Settings.PlayerName;
if(!string.IsNullOrEmpty(playerName))
args.AppendFormat(@"+seta name ""{0}"" ", playerName);
if(!string.IsNullOrEmpty(serverAddr))
args.AppendFormat(@"+connect ""{0}"" +seta server1 ""{0}""", serverAddr);
Process.Start(new ProcessStartInfo(path) {
Arguments = args.ToString(),
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(path),
}).WaitForExit();
} finally {
WindowState = FormWindowState.Normal;
Enabled = true;
}
}
public void LoadDefaultServer() {
if(!serverResetButton.Enabled)
return;
serverResetButton.Enabled = false;
bool serverAddressInputEnabled = serverAddressInput.Enabled;
serverAddressInput.Enabled = false;
serverAddressInput.Text = "Loading...";
new Thread(() => {
void reportError(string message, string caption = "Error while fetching default server") {
InvokeAction(() => {
MessageBox.Show(this, message, $"SoFii :: {caption}");
serverAddressInput.Text = string.Empty;
serverResetButton.Text = "Retry";
});
};
try {
using(DNSClient dns = new DNSClient()) {
DNSClient.Response resp = dns.QueryRecords(DNSClient.RecordType.TXT, Constants.SOFII_DOMAIN);
if(!resp.IsSuccess) {
reportError($"Could not resolve {Constants.SOFII_DOMAIN}.");
return;
}
string addr = string.Empty;
string port = string.Empty;
string ver = string.Empty;
StringBuilder messageBuilder = new StringBuilder();
foreach(DNSClient.Answer answer in resp.Answers) {
if(answer.Type != DNSClient.RecordType.TXT)
continue;
string text = answer.GetTXTData().Text.Trim();
if(text.StartsWith("msg=")) {
messageBuilder.AppendLine(text.Substring(4));
continue;
}
string[] textParts = text.Split(' ');
foreach(string textPart in textParts) {
if(textPart.StartsWith("addr=") && string.IsNullOrEmpty(addr))
addr = textPart.Substring(5);
else if(textPart.StartsWith("port=") && string.IsNullOrEmpty(port))
port = textPart.Substring(5);
else if(textPart.StartsWith("ver=") && string.IsNullOrEmpty(ver))
ver = textPart.Substring(4);
}
}
if(messageBuilder.Length > 0)
reportError(messageBuilder.ToString(), "Message from the server");
if(string.IsNullOrEmpty(addr)) {
if(messageBuilder.Length < 1)
reportError("Server did not respond with an address and did not supply additional information.");
return;
}
InvokeAction(() => {
serverResetButton.Text = "Reset";
string fullAddr = addr;
if(!string.IsNullOrEmpty(port))
fullAddr += $":{port}";
Settings.ServerAddress = fullAddr;
serverAddressInput.Text = fullAddr;
if(!string.IsNullOrEmpty(ver) && int.TryParse(ver, out int version)
&& Program.GetSemVerInt() < version && Settings.IgnoreNewVersion < version) {
DialogResult result = MessageBox.Show(
this,
"A newer version of SoFii is available that may include necessary and/or new features.\r\nCheck https://fii.moe/sofii for more information.\r\n\r\nPress OK to continue or Cancel if you don't want to be notified about this version again.\r\nThe Help button will open the information page in your default web browser.",
"SoFii :: New version notification",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Information,
MessageBoxDefaultButton.Button1,
0,
Constants.INFO_URL
);
if(result == DialogResult.Cancel)
Settings.IgnoreNewVersion = version;
}
});
}
} catch(Exception ex) {
#if DEBUG
reportError(ex.ToString());
#else
reportError(ex.Message);
#endif
} finally {
InvokeAction(() => {
serverAddressInput.Enabled = serverAddressInputEnabled;
serverResetButton.Enabled = true;
});
}
}) { IsBackground = true }.Start();
}
private void serverResetButton_Click(object sender, EventArgs e) {
Settings.UseDefaultServer = true;
serverAddressInput.Enabled = false;
serverUseCustom.Checked = false;
LoadDefaultServer();
}
private void gameBrowseButton_Click(object sender, EventArgs e) {
if(mainOpenFileDiag.ShowDialog() == DialogResult.OK) {
string path = mainOpenFileDiag.FileName;
if(CheckGameExists(path))
Settings.GamePath = path;
}
}
private void launchGameButton_Click(object sender, EventArgs e) {
LaunchGame();
}
private void serverUseCustom_CheckedChanged(object sender, EventArgs e) {
bool useCustom = serverUseCustom.Checked;
bool useDefault = !useCustom;
if(Settings.UseDefaultServer != useDefault)
Settings.UseDefaultServer = useDefault;
serverAddressInput.Enabled = useCustom;
if(useDefault)
LoadDefaultServer();
}
private void mainTabs_SelectedIndexChanged(object sender, EventArgs e) {
ResizeToFit();
}
public void DrawPlayerNamePreview() {
string userName = playerNameInput.Text;
bool nextIsColourChar = false;
Color colour = SystemColors.ControlText;
Brush brush = new SolidBrush(colour);
Font font = SystemFonts.DefaultFont;
float offset = 0;
StringBuilder sb = new StringBuilder();
using(Graphics gfx = playerNamePreview.CreateGraphics()) {
gfx.Clear(SystemColors.ControlLightLight);
float spaceWidth = gfx.MeasureString(" ", font).Width + .1f;
void drawBuffer() {
if(sb.Length < 1)
return;
string str = sb.ToString();
sb.Length = 0;
gfx.DrawString(str, font, brush, offset, 0);
offset += gfx.MeasureString(str, font).Width - spaceWidth;
};
foreach(char chr in userName) {
if(nextIsColourChar) {
foreach(ColourCharInfo cci in Colours)
if(cci.Char == chr) {
if(cci.Colour != colour) {
colour = cci.Colour;
brush.Dispose();
brush = new SolidBrush(colour);
}
break;
}
nextIsColourChar = false;
continue;
}
if(chr == '^') {
nextIsColourChar = true;
drawBuffer();
continue;
}
sb.Append(chr);
}
drawBuffer();
}
brush.Dispose();
}
private void playerNameInput_TextChanged(object sender, EventArgs e) {
Settings.PlayerName = playerNameInput.Text;
DrawPlayerNamePreview();
}
private void gfxFullscreen_CheckedChanged(object sender, EventArgs e) {
Settings.GfxFullscreen = gfxFullscreen.Checked;
}
private void gfxResolutionSelect_SelectedIndexChanged(object sender, EventArgs e) {
if(gfxResolutionSelect.SelectedItem is SoF2VideoMode vidMode)
Settings.GfxMode = vidMode.Mode;
else
Settings.Remove(Settings.GFX_MODE);
}
}
}