diff --git a/PKHeX.Core/Editing/Showdown/ShowdownTeam.cs b/PKHeX.Core/Editing/Showdown/ShowdownTeam.cs
new file mode 100644
index 000000000..c27e1387a
--- /dev/null
+++ b/PKHeX.Core/Editing/Showdown/ShowdownTeam.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace PKHeX.Core;
+
+///
+/// Logic for retrieving Showdown teams from URLs.
+///
+public static class ShowdownTeam
+{
+ ///
+ /// Generates the API URL for retrieving a Showdown team based on the supplied team identifier.
+ ///
+ /// The numeric identifier of the team.
+ /// A string containing the full URL to access the team data via the API.
+ public static string GetTeamURL(int team) => $"https://play.pokemonshowdown.com/api/getteam?teamid={team}&raw=1";
+
+ ///
+ /// Attempts to retrieve the Showdown team data from a specified URL, and reformats it.
+ ///
+ /// The URL to retrieve the team data from.
+ /// When the method returns, contains the processed team data if retrieval and formatting succeed; otherwise, null.
+ /// The numeric identifier extracted from the URL or response.
+ /// true if the team data is successfully retrieved and reformatted; otherwise, false.
+ public static bool TryGetTeams(string url, [NotNullWhen(true)] out string? content, out int team)
+ {
+ team = 0;
+ content = null;
+ if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult) || (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
+ return false;
+
+ content = NetUtil.GetStringFromURL(uriResult);
+ if (content == null)
+ return false;
+
+ return GetTeamsFromReply(ref content);
+ }
+
+ ///
+ /// Extracts the team data from the API reply and reformats it by replacing escaped newline
+ /// characters with system-specific line breaks.
+ ///
+ ///
+ /// A reference to the API response string. On successful extraction, the value is replaced
+ /// with the reformatted team data; otherwise, it remains unchanged.
+ ///
+ ///
+ /// true if the team data is successfully extracted and reformatted; otherwise, false.
+ ///
+ public static bool GetTeamsFromReply(ref string content)
+ {
+ // reformat
+ const string startText = """
+ "team":"
+ """;
+ var start = content.IndexOf(startText, StringComparison.Ordinal);
+ if (start == -1)
+ return false;
+ start += startText.Length; // skip to the start of the team
+
+ var end = content.LastIndexOf("\\n", StringComparison.Ordinal);
+ if (end == -1 || end <= start)
+ return false;
+
+ content = content[start..end].Replace("\\n", Environment.NewLine);
+ return true;
+ }
+
+ ///
+ /// Determines if the provided text is a valid Showdown team URL. If valid, returns a normalized API URL.
+ ///
+ /// The text to evaluate.
+ /// When the method returns, contains the normalized API URL if the text represents a valid Showdown team URL; otherwise, null.
+ /// true if the text is a valid Showdown team URL; otherwise, false.
+ public static bool IsShowdownTeamURL(ReadOnlySpan text, [NotNullWhen(true)] out string? url)
+ {
+ text = text.Trim();
+ if (text.StartsWith("https://psim.us/t/") || // short link
+ text.StartsWith("https://teams.pokemonshowdown.com/"))
+ return TryGetTeamFromWebURL(text, out url);
+
+ if (text.StartsWith("https://play.pokemonshowdown.com/api/getteam?teamid="))
+ return TryGetTeamFromAPITeamURL(text, out url);
+
+ url = null;
+ return false;
+ }
+
+ ///
+ /// Attempts to extract the team identifier from a Showdown web URL and converts it to a standard API URL.
+ ///
+ /// The Showdown web URL as a read-only span of characters.
+ /// When the method returns, contains the standardized API URL if extraction is successful; otherwise, null.
+ /// true if the team index is successfully extracted and converted; otherwise, false.
+ public static bool TryGetTeamFromWebURL(ReadOnlySpan text, [NotNullWhen(true)] out string? url)
+ {
+ url = null;
+ if (!TryGetTeamIndexWeb(text, out var team))
+ return false;
+ url = GetTeamURL(team);
+ return true;
+ }
+
+ ///
+ /// Attempts to extract the team identifier from a Showdown API URL and returns a standardized API URL.
+ ///
+ /// The Showdown API URL as a read-only span of characters.
+ /// When the method returns, contains the standardized API URL if extraction is successful; otherwise, null.
+ /// true if the team index is successfully extracted and the URL normalized; otherwise, false.
+ public static bool TryGetTeamFromAPITeamURL(ReadOnlySpan text, [NotNullWhen(true)] out string? url)
+ {
+ url = null;
+ if (!TryGetTeamIndexAPI(text, out var team))
+ return false;
+ url = GetTeamURL(team);
+ return true;
+ }
+
+ ///
+ /// Extracts the team identifier from a Showdown web URL.
+ ///
+ /// The Showdown web URL provided as a read-only span of characters.
+ /// When the method returns, contains the extracted team identifier if successful; otherwise, zero.
+ /// true if the team identifier is successfully extracted; otherwise, false.
+ public static bool TryGetTeamIndexWeb(ReadOnlySpan text, out int team)
+ {
+ team = 0;
+ if (text.EndsWith('/'))
+ text = text[..^1]; // remove trailing slash
+ if (text.EndsWith("/raw"))
+ text = text[..^4]; // remove trailing /raw
+
+ int start = text.LastIndexOf('/'); // seek back to =
+ if (start == -1)
+ return false;
+
+ var number = text[(start + 1)..];
+ if (!int.TryParse(number, out team))
+ return false;
+ return true;
+ }
+
+ ///
+ /// Extracts the team identifier from a Showdown API URL.
+ ///
+ /// The Showdown API URL as a read-only span of characters.
+ /// When the method returns, contains the extracted team identifier if successful; otherwise, zero.
+ /// true if the team identifier is successfully extracted; otherwise, false.
+ public static bool TryGetTeamIndexAPI(ReadOnlySpan text, out int team)
+ {
+ team = 0;
+ if (!text.EndsWith("&raw=1"))
+ return false;
+
+ text = text[..^6];
+ int start = text.LastIndexOf('='); // seek back to =
+ if (start == -1)
+ return false;
+
+ var number = text[(start + 1)..];
+ if (!int.TryParse(number, out team))
+ return false;
+ return true;
+ }
+}
diff --git a/PKHeX.Core/Util/NetUtil.cs b/PKHeX.Core/Util/NetUtil.cs
index 894d32e2d..2b2324e03 100644
--- a/PKHeX.Core/Util/NetUtil.cs
+++ b/PKHeX.Core/Util/NetUtil.cs
@@ -30,6 +30,7 @@ public static class NetUtil
{
// The GitHub API will fail if no user agent is provided
using var client = new HttpClient();
+ client.Timeout = TimeSpan.FromSeconds(3);
const string agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
client.DefaultRequestHeaders.Add("User-Agent", agent);
var response = client.GetAsync(url).Result;
diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs
index 173fc70fd..55f5bc413 100644
--- a/PKHeX.WinForms/MainWindow/Main.cs
+++ b/PKHeX.WinForms/MainWindow/Main.cs
@@ -524,7 +524,11 @@ private void ClickShowdownImportPKM(object sender, EventArgs e)
// Get Simulator Data
var text = Clipboard.GetText();
- var set = new ShowdownSet(text);
+ ShowdownSet set;
+ if (ShowdownTeam.IsShowdownTeamURL(text, out var url) && ShowdownTeam.TryGetTeams(url, out var content, out _))
+ set = ShowdownParsing.GetShowdownSets(content).FirstOrDefault() ?? new(""); // take only first set
+ else
+ set = new ShowdownSet(text);
if (set.Species == 0)
{ WinFormsUtil.Alert(MsgSimulatorFailClipboard); return; }