Files
DorkNet/launcher/PhotonWizard.xaml.cs
T
Alexa 7c974e39ab launcher: Photon AppId setup walkthrough
The two empty "REALTIME APPID / VOICE APPID" textboxes assumed the
user already knew what Photon was, where to sign up, and which app
type to create. New 5-step wizard fills that gap:

1. What's Photon? — one-paragraph explanation tying back to DorkNet
2. Make an account — Open Dashboard button launches browser
3. Create a Realtime app — numbered click-by-click instructions
4. Create a Voice app — same, with a Skip button (voice is optional)
5. Paste your AppIds — text fields with live regex validation,
   region picker, Save commits to AppState

Hyphens optional on input; we normalise to canonical 8-4-4-4-12 form
before writing back. The existing Photon section keeps its three
fields for advanced users, with a "Setup walkthrough" button at the
top opening the wizard prefilled with current values.
2026-05-27 11:14:23 +03:00

190 lines
7.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using DorkNet.Launcher.Backend;
namespace DorkNet.Launcher;
/// <summary>Five-step walkthrough for first-time Photon setup. Opens
/// dashboard.photonengine.com on step 2, walks the user through app
/// creation in steps 3–4, then collects + validates AppIds on step 5.
///
/// <para>Result lives in <see cref="Result"/> when <see cref="DialogResult"/>
/// is true. Caller (MainWindow) writes it back into AppState.</para></summary>
public partial class PhotonWizard : Window
{
public PhotonResult? Result { get; private set; }
// Photon AppIds are GUIDs — accept with or without hyphens; user
// gets confused if we're strict about formatting. We render the
// canonical form back into AppState.
private static readonly Regex AppIdRegex =
new("^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$",
RegexOptions.Compiled);
private readonly StackPanel[] _steps;
private int _stepIndex;
public PhotonWizard(string? existingRealtime, string? existingVoice, string? existingRegion)
{
InitializeComponent();
_steps = new[] { Step1, Step2, Step3, Step4, Step5 };
// Pre-fill so a user re-opening the wizard sees their existing
// values — they might just want to fix the region or swap apps.
WizRealtime.Text = existingRealtime ?? "";
WizVoice.Text = existingVoice ?? "";
SelectRegion(existingRegion ?? "us");
UpdateAppIdStatuses();
ShowStep(0);
}
private void ShowStep(int index)
{
_stepIndex = Math.Clamp(index, 0, _steps.Length - 1);
for (var i = 0; i < _steps.Length; i++)
_steps[i].Visibility = i == _stepIndex ? Visibility.Visible : Visibility.Collapsed;
StepIndicator.Text = $"STEP {_stepIndex + 1} OF {_steps.Length}";
PrevBtn.Visibility = _stepIndex == 0 ? Visibility.Collapsed : Visibility.Visible;
// Step 4 (Voice) is optional — surface a Skip button so the user
// doesn't feel stuck if they don't want voice chat. Final step
// swaps Next for a Save action.
SkipBtn.Visibility = _stepIndex == 3 ? Visibility.Visible : Visibility.Collapsed;
NextBtn.Content = _stepIndex == _steps.Length - 1 ? "Save" : "Next →";
}
private void OnPrev(object sender, RoutedEventArgs e) => ShowStep(_stepIndex - 1);
private void OnNext(object sender, RoutedEventArgs e)
{
if (_stepIndex < _steps.Length - 1)
{
ShowStep(_stepIndex + 1);
return;
}
TrySave();
}
private void OnClose(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void OnOpenPhotonDashboard(object sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo("https://dashboard.photonengine.com")
{ UseShellExecute = true });
}
catch (Exception ex)
{
MessageBox.Show(this, "Could not open the dashboard: " + ex.Message,
"Photon setup", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private void OnAppIdChanged(object sender, TextChangedEventArgs e) => UpdateAppIdStatuses();
private void UpdateAppIdStatuses()
{
var realtime = WizRealtime.Text.Trim();
if (string.IsNullOrEmpty(realtime))
{
WizRealtimeStatus.Text = "32-character ID with hyphens, like 12ab34cd-56ef-78ab-90cd-12ef34ab56cd";
WizRealtimeStatus.Foreground = (System.Windows.Media.Brush)FindResource("InkFaint");
}
else if (AppIdRegex.IsMatch(realtime))
{
WizRealtimeStatus.Text = "✓ Looks like a valid AppId.";
WizRealtimeStatus.Foreground = (System.Windows.Media.Brush)FindResource("Ok");
}
else
{
WizRealtimeStatus.Text = "⚠ That doesn't look like a Photon AppId. Should be 32 hex characters (with or without hyphens).";
WizRealtimeStatus.Foreground = (System.Windows.Media.Brush)FindResource("Danger");
}
var voice = WizVoice.Text.Trim();
if (string.IsNullOrEmpty(voice))
{
WizVoiceStatus.Text = "Optional — leave empty if you skipped step 4.";
WizVoiceStatus.Foreground = (System.Windows.Media.Brush)FindResource("InkFaint");
}
else if (AppIdRegex.IsMatch(voice))
{
WizVoiceStatus.Text = "✓ Looks like a valid AppId.";
WizVoiceStatus.Foreground = (System.Windows.Media.Brush)FindResource("Ok");
}
else
{
WizVoiceStatus.Text = "⚠ That doesn't look like a Photon AppId.";
WizVoiceStatus.Foreground = (System.Windows.Media.Brush)FindResource("Danger");
}
}
private void TrySave()
{
var realtime = WizRealtime.Text.Trim();
var voice = WizVoice.Text.Trim();
if (string.IsNullOrEmpty(realtime))
{
MessageBox.Show(this,
"A Realtime AppId is required — without it nobody can connect to your server. Voice is optional.",
"Photon setup", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!AppIdRegex.IsMatch(realtime))
{
MessageBox.Show(this,
"Your Realtime AppId doesn't look right. It should be 32 hex characters (with or without hyphens). Double-check that you copied the entire string from the Photon dashboard.",
"Photon setup", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!string.IsNullOrEmpty(voice) && !AppIdRegex.IsMatch(voice))
{
MessageBox.Show(this,
"Your Voice AppId doesn't look right. Either fix it or clear the field to skip voice.",
"Photon setup", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var region = WizRegion.SelectedItem is ComboBoxItem item && item.Tag is not null
? item.Tag.ToString() ?? "us"
: "us";
Result = new PhotonResult(Normalise(realtime), Normalise(voice), region);
DialogResult = true;
Close();
}
private void SelectRegion(string tag)
{
foreach (var item in WizRegion.Items.OfType<ComboBoxItem>())
{
if (string.Equals(item.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase))
{
item.IsSelected = true;
return;
}
}
}
/// <summary>Render the AppId in canonical 8-4-4-4-12 form regardless
/// of how the user pasted it — the server's Photon SDK accepts both
/// but normalising keeps the rest of the launcher tidy.</summary>
private static string Normalise(string appId)
{
if (string.IsNullOrEmpty(appId)) return "";
var stripped = appId.Replace("-", "").Trim();
if (stripped.Length != 32) return appId;
return $"{stripped[..8]}-{stripped[8..12]}-{stripped[12..16]}-{stripped[16..20]}-{stripped[20..]}";
}
}
public sealed record PhotonResult(string RealtimeAppId, string VoiceAppId, string Region);