Editor/Core/RouteFailureRules.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TeamCreate;
/// <summary>One route a join attempt tried, and how it ended.</summary>
public sealed record RouteAttempt(string Source, string Target, string Reason, string RelayCode = null);
public static class RouteFailureRules
{
public const int MaxLength = 480;
/// <summary>
/// Turns "every route failed" into something a person can act on. The raw socket message ("the target
/// machine actively refused it") names a symptom on one address; what a friend needs is which routes were
/// tried and what to ask the host to change, and that differs by which routes the code actually offered.
/// </summary>
public static string Summarize(IReadOnlyList<RouteAttempt> attempts)
{
if (attempts == null || attempts.Count == 0)
{
return "This invite code offers no route to the host. Ask the host for a new code.";
}
bool relayTried = attempts.Any(a => a.Source == RelayProtocol.RouteSource);
RouteAttempt relayAttempt = attempts.FirstOrDefault(a => a.Source == RelayProtocol.RouteSource);
bool directTried = attempts.Any(a => a.Source == "code" || a.Source == "advanced");
StringBuilder text = new StringBuilder("Could not reach the host. ");
bool loopback = relayAttempt != null && relayAttempt.RelayCode == RelayProtocol.LoopbackCode;
if (relayAttempt != null)
{
text.Append(loopback
? "This code points to a relay on the host's own computer, which only works for the host. "
: relayAttempt.RelayCode == "no-host"
? "The relay is reachable but the host is not connected to it. "
: "The relay route failed (" + Trim(relayAttempt.Reason) + "). ");
}
RouteAttempt direct = attempts.FirstOrDefault(a => a.Source == "code");
if (direct != null)
{
text.Append("The host's public address " + direct.Target + " did not accept a connection (" + Trim(direct.Reason) + "). ");
}
if (!relayTried && directTried)
{
text.Append("This code has no relay route, so it depends on the host's router forwarding the session port. ");
}
if (loopback)
{
text.Append("Ask the host to start a new session and send a new code.");
}
else if (relayAttempt != null && relayAttempt.RelayCode == "no-host")
{
text.Append("Ask the host to start, or restart, the session and send a new code.");
}
else if (!relayTried)
{
text.Append("Ask the host to configure a relay (RelayUrl in ProjectSettings/collaboration.json), to forward the port, or join from the same network.");
}
else
{
text.Append("Try again in a moment, or ask the host to confirm the relay is running.");
}
return CollaborationDiagnostics.Bounded(text.ToString().Trim(), MaxLength);
}
private static string Trim(string reason)
{
if (string.IsNullOrWhiteSpace(reason))
{
return "no reason given";
}
string one = reason.Replace('\r', ' ').Replace('\n', ' ').Trim();
return one.Length <= 90 ? one.TrimEnd('.') : one.Substring(0, 90).TrimEnd('.') + "…";
}
}
/// <summary>
/// The relay access key is a credential a host presents to create a room. It lives in an untracked local
/// file rather than the shared project settings, so committing the project never publishes it.
/// </summary>
public static class RelayAccessKeyFile
{
public const string FileName = "relay-access-key.txt";
/// <summary>Reads the first non-blank, non-comment line. Returns null when there is none, and an error when it is unusable.</summary>
public static string Parse(string text, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
foreach (string raw in text.Split('\n'))
{
string line = raw.Trim();
if (line.Length == 0 || line[0] == '#')
{
continue;
}
if (line.Length > RelayProtocol.MaxAccessKeyChars)
{
error = "The relay access key is longer than " + RelayProtocol.MaxAccessKeyChars + " characters.";
return null;
}
foreach (char c in line)
{
if (char.IsControl(c) || char.IsWhiteSpace(c))
{
error = "The relay access key must be a single word with no spaces or control characters.";
return null;
}
}
return line;
}
return null;
}
}