diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs
index d16d02e..b8595ae 100644
--- a/overlay/oxide/plugins/RunicGateway.cs
+++ b/overlay/oxide/plugins/RunicGateway.cs
@@ -65,6 +65,15 @@ namespace Oxide.Plugins
///
private const int MaxBackoffMs = 5000;
+ ///
+ /// How long a link must stay up before its drop counts as an outage rather than a
+ /// refusal. The backoff resets only past it: a sidecar that closes every connection at
+ /// once — one refusing a plugin that names another server (D155), or one crash-looping —
+ /// would otherwise be redialled twice a second for ever, rebuilding the boards on the
+ /// main thread and printing two console lines each time.
+ ///
+ private const int StableLinkMs = 10000;
+
/// The longest line accepted from the sidecar. Over-long lines are discarded.
private const int MaxInboundLineBytes = 1024 * 1024;
@@ -244,6 +253,13 @@ namespace Oxide.Plugins
_bootId = ResolveBootId();
_wipeId = ResolveWipeId();
+
+ // Before the link thread, not in `Loaded`: both frameworks call `Init` first, and a
+ // thread started here with `_config` still null dials the 7799 fallback. With one
+ // server per host that is its own sidecar by coincidence; with two it is the OTHER
+ // server's, whose sidecar then files this server's history under that one's id
+ // (found by the phase 18 walk, D154).
+ LoadConfigValues();
_running = true;
// Expired link codes are dropped on a timer, not on lookup: an unconfirmed code is
@@ -496,7 +512,6 @@ namespace Oxide.Plugins
private void Loaded()
{
- LoadConfigValues();
Puts($"protocol {ProtocolVersion}, serverId '{_config.ServerId}', sidecar {_config.Host}:{_config.Port}");
// Here rather than at `OnServerInitialized`: the link thread starts in `Init`, so a
@@ -564,6 +579,7 @@ namespace Oxide.Plugins
{
TcpClient client = null;
Thread reader = null;
+ System.Diagnostics.Stopwatch upFor = null;
int epoch = Interlocked.Increment(ref _epoch);
@@ -582,7 +598,7 @@ namespace Oxide.Plugins
_dead = false;
_connected = true;
_loggedConnectFailure = false; // so a LATER outage is reported again
- backoffMs = 500;
+ upFor = System.Diagnostics.Stopwatch.StartNew();
Interlocked.Increment(ref _connects);
Puts($"connected to {host}:{port}");
@@ -654,14 +670,25 @@ namespace Oxide.Plugins
if (!_running)
break;
+ // A link that held is an outage: start again from the shortest wait. One that was
+ // closed on arrival keeps doubling (see StableLinkMs).
+ if (upFor != null && upFor.ElapsedMilliseconds >= StableLinkMs)
+ backoffMs = 500;
+
// Nothing is listening yet, or the sidecar restarted. Both are normal.
//
- // `Wake`, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one, so
- // an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload —
- // Oxide reports it as `Calling 'Unload' took 1918ms`, and the server is frozen for
- // that whole time. Unload sets `_running` false and signals `Wake`, so waiting on
- // the handle turns that stall into an immediate return.
- Wake.WaitOne(backoffMs);
+ // A handle, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one,
+ // so an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload
+ // — Oxide reports it as `Calling 'Unload' took 1918ms`, and the server is frozen for
+ // that whole time. Unload sets `_stopping`, so waiting on it turns that stall into
+ // an immediate return.
+ //
+ // `_stopping`, not `Wake`, for the reason `_stopping` gives: `Emit` signals `Wake`
+ // on every frame, so a backoff waited on it lasted until the next hook fired. That
+ // was invisible while a failed connect was silent; against a sidecar that refuses
+ // (D155) the phase 18 walk measured some 250 redials a second, each printing two
+ // console lines and rebuilding the boards.
+ _stopping.WaitOne(backoffMs);
backoffMs = Math.Min(backoffMs * 2, MaxBackoffMs);
}