Skip to content

Add support for V3 tunnels#845

Open
11EJDE11 wants to merge 47 commits intoCnCNet:developfrom
11EJDE11:new-v3-tunnels
Open

Add support for V3 tunnels#845
11EJDE11 wants to merge 47 commits intoCnCNet:developfrom
11EJDE11:new-v3-tunnels

Conversation

@11EJDE11
Copy link
Member

@11EJDE11 11EJDE11 commented Oct 11, 2025

Closes #349

  • V2 tunnels remain fully supported.
  • Adds V3 Static tunnels: One host-chosen tunnel shared by all players (similar to V2).
  • Adds V3 Dynamic tunnels: Each pair of players automatically negotiates the optimal tunnel between them.

New UserINISettings

Setting Description
UseLegacyTunnels If on: games use V2 tunnels (overrides UseDynamicTunnels).
If off: games use V3 tunnels.
UseDynamicTunnels If on: lobbies start with no predefined tunnel; tunnel negotiations occurs as players join.
If off: games use static V3 tunnels (unless legacy mode is enabled).

New Lobby Commands

Command Description
/tunnelmode [<mode>] Cycles through tunnel modes or sets one explicitly.
Modes:
0 - V3 (static)
1 - V3 (dynamic)
2 - V2 (legacy)
/negstatus Opens the negotiation status panel, showing per-pair negotiation progress and measured pings.
/tunnelinfo Modified to display the current tunnel version. Doesn't display info when dynamic tunnels are enabled.

New CTCP Messages

Command Description
PLYTNL <address>:<port> Announces which tunnel a player pair is using (sent by the decider).
NEGINFO <target_player>;status[;<ping>] Broadcasts negotiation progress and ping results.
TNLRENEG <failed_address>:<failed_port> Requests renegotiation if a tunnel fails in dynamic mode.
TNLFAIL <tunnel_name> Reports a failed tunnel to other players.
STARTV2 / STARTV3 START has been versioned into STARTV2 and STARTV3.

How V3 Tunnels Work

In V2, all players share a single tunnel.
Example:

Player1 (Australia)
Player2 (New Zealand)
Player3 (England)
Tunnel: France

For P1 to send data to P2:
P1 Australia -> Tunnel France -> P2 NZ  = 550ms

In V3 dynamic mode, each pair of players negotiates the best tunnel route:

P1 <--> P2  -> Tunnel: Australia (50ms)
P1 <--> P3  -> Tunnel: France (300ms)
P2 <--> P3  -> Tunnel: Seattle (310ms)
So now for P1 to send data to P2:
P1 Australia > Tunnel Australia > P2 NZ = 50ms (90.9% faster)
And the slowest link is now 310ms instead of 550ms (43.6% faster overall).

The client now acts as an intermediary: the game connects to the client, which then routes packets to the negotiated tunnels. This is for both static and dynamic V3.


Negotiation Process

Negotiations occur automatically when players join a lobby and take a few seconds once the player list has arrived.

  1. Each player pair tests all available tunnels by exchanging 5 UDP pings per tunnel.
    RTT, packet loss, and average latency are measured.
  2. One player is designated as the decider (the one with the higher player ID).
    • The decider receives Connected packets and sends PingRequest packets.
    • The non-decider responds with PingResponse packets.
  3. Once 80% of tunnels have been tested, the decider picks the best tunnel and informs the non-decider.
  4. The non-decider acknowledges, completing negotiation.

If Ping unofficial CnCNet tunnels is disabled, you will only negotiate over official tunnels.


Example Log Output (truncated)

09.10. 12:18:15.170    === Negotiation Results for QWE21 (ID: 3760759627) ===
Player: QWE21 | Tunnel: CnCNet Australia | Avg RTT: 65.2ms | Real ping: 34.0ms | Real ping*2: 68.0ms | Difference: -2.8ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: CnCNet Europe | Avg RTT: 583.5ms | Real ping: 284.0ms | Real ping*2: 568.0ms | Difference: 15.5ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: CnCNet Japan | Avg RTT: 291.3ms | Real ping: 193.0ms | Real ping*2: 386.0ms | Difference: -94.7ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: CnCNet Singapore | Avg RTT: 256.3ms | Real ping: 123.0ms | Real ping*2: 246.0ms | Difference: 10.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: United-Forum.de | Avg RTT: N/A | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: "[EU] Fast Server HA Germany leardev.de" | Avg RTT: 633.7ms | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [ EJ ] N. America (California) | Avg RTT: 269.9ms | Real ping: 140.0ms | Real ping*2: 280.0ms | Difference: -10.1ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [CN][JS]CORA_SERVER | Avg RTT: 349.1ms | Real ping: 176.0ms | Real ping*2: 352.0ms | Difference: -2.9ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [CN][JS]MonkeyRay's Server #1 | Avg RTT: 371.3ms | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [CN][NBO]long_ken's Server #1 | Avg RTT: 393.3ms | Real ping: 192.0ms | Real ping*2: 384.0ms | Difference: 9.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [CN][SH]REV_v3_N | Avg RTT: 393.3ms | Real ping: 186.0ms | Real ping*2: 372.0ms | Difference: 21.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [DE]XuanServerDE | Avg RTT: 591.3ms | Real ping: 288.0ms | Real ping*2: 576.0ms | Difference: 15.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [EU] Kisiek.net | Avg RTT: 560.9ms | Real ping: 284.0ms | Real ping*2: 568.0ms | Difference: -7.1ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [EU] Z Server MP https://zserver.org | Avg RTT: 592.2ms | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [TR] CK Server | Avg RTT: 653.1ms | Real ping: 307.0ms | Real ping*2: 614.0ms | Difference: 39.1ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [TW] Jun's Tunnel Server | Avg RTT: 380.7ms | Real ping: 192.0ms | Real ping*2: 384.0ms | Difference: -3.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [UK] London | Avg RTT: 547.7ms | Real ping: 275.0ms | Real ping*2: 550.0ms | Difference: -2.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [US West] Z Server MP https://zserver.org | Avg RTT: 317.4ms | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [US-C] Bot.Rip CNC Relay | Avg RTT: 363.6ms | Real ping: 180.0ms | Real ping*2: 360.0ms | Difference: 3.6ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [US-LA]XuanServerUS-LA | Avg RTT: 289.0ms | Real ping: 140.0ms | Real ping*2: 280.0ms | Difference: 9.0ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: [US-NY]XuanServerUS-NY | Avg RTT: N/A | Real ping: 305.0ms | Real ping*2: 610.0ms | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: [US][EU] Coolissy | Avg RTT: 306.0ms | Real ping: 152.0ms | Real ping*2: 304.0ms | Difference: 2.0ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: aliyunbj.cdn.leohearts.com | Avg RTT: N/A | Real ping: 217.0ms | Real ping*2: 434.0ms | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: AsSaltJordan | Avg RTT: 694.1ms | Real ping: 347.0ms | Real ping*2: 694.0ms | Difference: 0.1ms | Packet Loss: 20.0% | Pings: 4/5 | Connected: True
Player: QWE21 | Tunnel: Bloody Eye | Avg RTT: 422.5ms | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: bot.rip-eastus | Avg RTT: 560.0ms | Real ping: 284.0ms | Real ping*2: 568.0ms | Difference: -8.0ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: ISS Server (HK) | Avg RTT: 322.6ms | Real ping: 163.0ms | Real ping*2: 326.0ms | Difference: -3.4ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: KFCV50-BeiJing-1-mindwhy.top-by-2049265547@qq.com- | Avg RTT: 416.0ms | Real ping: 205.0ms | Real ping*2: 410.0ms | Difference: 6.0ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: KFCV50-BeiJing-2-mindwhy.top-by-2049265547@qq.com- | Avg RTT: 416.0ms | Real ping: 207.0ms | Real ping*2: 414.0ms | Difference: 2.0ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: KFCV50-QingDao-mindwhy.top-by-2049265547@qq.com- | Avg RTT: N/A | Real ping: 201.0ms | Real ping*2: 402.0ms | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: Los Angeles Server | Avg RTT: 293.9ms | Real ping: 139.0ms | Real ping*2: 278.0ms | Difference: 15.9ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: MangosServer - NY | Avg RTT: 416.0ms | Real ping: 199.0ms | Real ping*2: 398.0ms | Difference: 18.0ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: Mixyzzs Server | Avg RTT: N/A | Real ping: 193.0ms | Real ping*2: 386.0ms | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: Mytunnel | Avg RTT: 23.8ms | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: Mytunnel | Avg RTT: 534.3ms | Real ping: 266.0ms | Real ping*2: 532.0ms | Difference: 2.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: PL TurnWar - turn-guild.ru | Avg RTT: 567.5ms | Real ping: 281.0ms | Real ping*2: 562.0ms | Difference: 5.5ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: RedAlert2.com the best | Avg RTT: 544.7ms | Real ping: 274.0ms | Real ping*2: 548.0ms | Difference: -3.3ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: RMMD UK Tunnel | Avg RTT: 544.9ms | Real ping: 271.0ms | Real ping*2: 542.0ms | Difference: 2.9ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: RU MSK TurnWar - turn-guild.ru | Avg RTT: 649.1ms | Real ping: 296.0ms | Real ping*2: 592.0ms | Difference: 57.1ms | Packet Loss: 0.0% | Pings: 5/5 | Connected: True
Player: QWE21 | Tunnel: Studio DNA | Avg RTT: N/A | Real ping: N/A | Real ping*2: N/A | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: Wick-Server | Avg RTT: N/A | Real ping: 196.0ms | Real ping*2: 392.0ms | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
Player: QWE21 | Tunnel: zz-tunnel | Avg RTT: N/A | Real ping: 349.0ms | Real ping*2: 698.0ms | Difference: N/A | Packet Loss: 100.0% | Pings: 0/0 | Connected: False
BEST TUNNEL for QWE21: Mytunnel (RTT: 23.8ms, Loss: 0.0%)
=== End Results for QWE21 ===

Tunnel Failure Handling

A new TunnelFailed event has been added to the TunnelHandler.

  • Dynamic tunnels:
    Automatically trigger renegotiation when a connection goes down (only affected players).

  • Static tunnels:

    • Hosts automatically select the next best tunnel if their tunnel fails.
    • Non-hosts broadcast failure messages so the host can respond (switch tunnel or kick the player).

Still to come...

  • GameLoadingLobby integration
  • Peer-to-Peer (P2P) connections
  • In-game tunnel monitoring and hot-swapping

Showing the negotiation in the lobby, and the negotiation status panel (available for all players).
image

Showing a change to the game creation window and a game's ping:
image

New settings:
image

Add V3GameTunnelBridge
Add V3TunnelCommunicator
Add V3TunnelNegotiator
Add TunnelFailed event
Update supported tunnel versions
Fixup GameCreationWindow control visibility
Remove unnecessary tunnel parameter in V3PlayerInfo
Add NegotiationStatusPanel
Add tunnel negotiations to CnCNetGameLobby
Add V3 tunnel support to CnCNetGameLobby
Move to file-scoped namespaces
Fix missing texture on status panel
Fix issue with decider seeing OK instead of ping results
Fix an issue with automatic tunnel selection
Style fixups
Split TunnelChosenEventArgs into new file
Remove IDisposable on bridge
Remove unecessary code
Better updating of player ping indicators
@github-actions
Copy link

github-actions bot commented Oct 11, 2025

Nightly build for this pull request:

  • artifacts.zip
    This comment is automatic and is meant to allow guests to get latest automatic builds without registering. It is updated on every successful build.

@CnCRAZER
Copy link
Contributor

Perhaps the ability to add a button within one of the clients ini files to toggle between V2 and V3 would be useful. Just a thought

@@ -0,0 +1,248 @@
using System;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write #nullable enable since you are using the nullable syntax. Same for other new files

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done now.

tunnel.PingInMs = pingResult;
}

if (previousPing > 0 && (tunnel.PingInMs <= 0 || tunnel.PingInMs > TUNNEL_FAILED_PING_AMOUNT))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 0 ms indicate a failure for now? Would be better using -1, in case some one runs a tunnel server AND plays the game, so the distance between tunnel server and the gaming computer is so close

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@11EJDE11

public override int Ping => TunnelServer?.PingInMs ?? 0;

0 or -1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also previousPing > 0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest fully go through the code to ensure the ping value successfully deals with 0 and -1. Alternatively to simplify the procedure and reduce error you might also want to wrap the ping int as a readonly record (so we can use something like ping.IsValid()), if needed

Comment on lines +406 to +407
if (GameTunnelBridge != null)
StopGameBridge();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the if statement necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed that. Thanks.

/// <summary>
/// Retrieves the <see cref="TunnelTestResult"/> for the specified tunnel, or null if not found.
/// </summary>
public TunnelTestResult GetTunnelResult(CnCNetTunnel tunnel) => TunnelResults.TryGetValue(tunnel, out var result) ? result : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering why you didn't continue using nullable syntax.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

public double GetBestPing()
{
var best = SelectBestTunnel();
return best != null ? TunnelResults[best].AverageRtt : double.NaN;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using Double.MaxValue? It seems that any comparison involving NaN (e.g., NaN > 5, NaN < 5, NaN == NaN) will result in false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I've changed that.

_localPlayer.Id,
_remotePlayer.Id,
TunnelPacketType.PingRequest,
BitConverter.GetBytes(i)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest crash if BitConverter.IsLittleEndian does not match our expected value, in case some extremely rare and weird machines break the protocol

I saw you are using BinaryPrimitives later. Maybe it is better than the machine-specific BitConverter methods here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, sorted now thanks.

_negotiationCts.Dispose();
_tunnelAckReceived.TrySetCanceled();
_negotiationCompletionSource.TrySetCanceled();
_tunnelHandler.UnregisterV3PacketHandler(_localPlayer.Id, _remotePlayer.Id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have used _tunnelHandler?UnregisterV3PacketHandler syntax but here you assume the field is not null. Perhaps it's better to enable nullable for all new files.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, sorted.

@SadPencil
Copy link
Member

SadPencil commented Nov 23, 2025

@11EJDE11 I suggest fully go through the code to ensure we successfully deal with 0 and -1 ping values. Currently there seems to be some mix usages between 0 and -1, e.g., previousPing > 0 and TunnelServer?.PingInMs ?? 0.

Alternatively to simplify the procedure and reduce error you might also want to wrap the ping int as a readonly record (so we can use something like ping.IsValid() to enhance readability), if needed

@11EJDE11
Copy link
Member Author

Sure, that makes sense, SP. I will take another look in the next day or two.

@11EJDE11
Copy link
Member Author

11EJDE11 commented Dec 2, 2025

@SadPencil, is the below what you had envisioned? If so, I will update the lobbies and players to all use this. There will be minor changes to a lot of files. If not, I can stick to int and there would be fewer changes required. Either is OK with me although I do prefer below.

public readonly record struct PingValue
{
    private readonly int _value;

    private PingValue(int value) => _value = value;

    /// <summary>
    /// An unknown or invalid ping measurement.
    /// </summary>
    public static PingValue Unknown => new(-1);

    /// <summary>
    /// Creates a PingValue from a millisecond amount.
    /// </summary>
    /// <param name="milliseconds">The ping in milliseconds. Must be non-negative.</param>
    /// <returns>A valid PingValue.</returns>
    /// <exception cref="ArgumentException">Thrown when milliseconds is negative.</exception>
    public static PingValue FromMs(int milliseconds)
    {
        if (milliseconds < 0)
            throw new ArgumentException("Ping cannot be negative. Use PingValue.Unknown for unknown pings.", nameof(milliseconds));
        return new(milliseconds);
    }

    /// <summary>
    /// Returns true if this ping value is valid (not unknown).
    /// </summary>
    public bool IsValid() => _value >= 0;

    /// <summary>
    /// Returns true if this ping value is unknown or invalid.
    /// </summary>
    public bool IsUnknown() => _value < 0;

    /// <summary>
    /// Gets the ping in milliseconds.
    /// Returns -1 for unknown pings.
    /// Prefer using IsValid() to check validity before accessing this value.
    /// </summary>
    public int Milliseconds => _value;

    /// <summary>
    /// Returns a string representation of theping value.
    /// </summary>
    public override string ToString() => IsValid() ? $"{_value}ms" : "Unknown";
}

@SadPencil
Copy link
Member

Yes!

Also, it's "0 ms" instead of "0ms" in your ToString implementation

@11EJDE11 11EJDE11 requested a review from SadPencil December 5, 2025 02:22
Copy link
Member

@Rampastring Rampastring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed up to V3PlayerInfo.cs, I'll have to continue another time.

The status panel seems to have a flaw regarding TextAnchor usage, otherwise I have only found stylistical issues so far. Overall it looks like good work.

0,
CELL_WIDTH,
HEADER_HEIGHT);
headerLabel.TextAnchor = LabelTextAnchorInfo.CENTER;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you set TextAnchor, you should also set AnchorPoint. The intention is probably centering the names on the cells - based on the screenshot that doesn't happen right now, and it would be an improvement to the interface.

See https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNALabel.cs#L75

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchAllGameModes = new BoolSetting(iniFile, MULTIPLAYER, "SearchAllGameModes", false);

UseLegacyTunnels = new BoolSetting(iniFile, MULTIPLAYER, "UseLegacyTunnels", false);
UseDynamicTunnels = new BoolSetting(iniFile, MULTIPLAYER, "UseDynamicTunnels", true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are mutually exclusive, making a single enum based (integer or string) TunnelMode setting instead could be a worthy idea. But this is OK too - doesn't significantly increase code complexity.

public const string QRES_EXECUTABLE = "qres.dat";

public const string CNCNET_PROTOCOL_REVISION = "R13";
public const string CNCNET_PROTOCOL_REVISION = "R15";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could save a character from GameSurge traffic by using alphabetics for the number instead.

RF for example. We could also drop the R to save two bytes - it used to be short for Rampastring to differentiate my protocol from Funky's client, but I don't think that's a major consideration anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +16 to +17
// reportingPlayer -> targetPlayer -> status
// This tracks what each player reports about their negotiation with each other player
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason these aren't /// <summary> type comments? It'd make IntelliSense pick them up and show them when you hover over the variable anywhere in code.

Same for _playerPingMatrix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +50 to +51
if (player1 == player2)
return NegotiationStatus.NotStarted;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can or should this ever happen? Should this be a condition for an exception instead for catching potential bugs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't happen, but never know what people will feed it in future. Changed 5b9f62b

Comment on lines +246 to +248
if (CurrentTunnel == null && otherPreviousPing.IsValid() &&
(pingResult.IsUnknown() || pingResult.Milliseconds > TUNNEL_FAILED_PING_AMOUNT))
TunnelFailed?.Invoke(this, otherTunnel);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the contribution guideline (Contributing.md):

Braceless code block bodies should be made only when both code block head and body are single line.

Suggested change
if (CurrentTunnel == null && otherPreviousPing.IsValid() &&
(pingResult.IsUnknown() || pingResult.Milliseconds > TUNNEL_FAILED_PING_AMOUNT))
TunnelFailed?.Invoke(this, otherTunnel);
if (CurrentTunnel == null && otherPreviousPing.IsValid() &&
(pingResult.IsUnknown() || pingResult.Milliseconds > TUNNEL_FAILED_PING_AMOUNT))
{
TunnelFailed?.Invoke(this, otherTunnel);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +68 to +70
foreach (var player in _otherPlayers)
if (player.Tunnel != null)
Logger.Log($" Player {player.Name}: {player.Tunnel.Address}:{player.Tunnel.Port}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the contribution guideline:

nested braceless blocks are not allowed within braceless blocks

Suggested change
foreach (var player in _otherPlayers)
if (player.Tunnel != null)
Logger.Log($" Player {player.Name}: {player.Tunnel.Address}:{player.Tunnel.Port}");
foreach (var player in _otherPlayers)
{
if (player.Tunnel != null)
Logger.Log($" Player {player.Name}: {player.Tunnel.Address}:{player.Tunnel.Port}");
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +81 to +82
/// Stops the game tunnel bridge, unregistering packet handlers,
/// and closed the local/game UDP socket.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Stops the game tunnel bridge, unregistering packet handlers,
/// and closed the local/game UDP socket.
/// Stops the game tunnel bridge, unregisters packet handlers,
/// and closes the local/game UDP socket.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +154 to +156
}
_tunnelHandler.SendPacket(recipient.Tunnel, _localId, recipient.Id,
TunnelPacketType.GameData, gameData);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
_tunnelHandler.SendPacket(recipient.Tunnel, _localId, recipient.Id,
TunnelPacketType.GameData, gameData);
}
_tunnelHandler.SendPacket(recipient.Tunnel, _localId, recipient.Id,
TunnelPacketType.GameData, gameData);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +158 to +159
else
Logger.Log($"V3GameTunnelBridge: No matching recipient found for receiverId={receiverId}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the contribution guidelines:

If you use if-else you should either have all of the code blocks braced or braceless to keep things consistent.

Suggested change
else
Logger.Log($"V3GameTunnelBridge: No matching recipient found for receiverId={receiverId}");
else
{
Logger.Log($"V3GameTunnelBridge: No matching recipient found for receiverId={receiverId}");
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CnCRAZER CnCRAZER requested review from Copilot and removed request for SadPencil March 1, 2026 00:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds V3 tunnel support (static + dynamic negotiation) while keeping V2 tunnels supported, including UI/status display and new settings/CTCP messages to coordinate tunnel mode and negotiation state across lobby participants.

Changes:

  • Introduces V3 tunnel transport/negotiation components (communicator, per-peer negotiator, and game UDP bridge).
  • Adds lobby UX for dynamic tunnel negotiation (status matrix/ping indicators) and new tunnel-mode/negstatus commands.
  • Reworks ping representation across multiplayer models to use PingValue (replacing int + -1 sentinel), and updates tunnel list selection/filtering for V2/V3.

Reviewed changes

Copilot reviewed 26 out of 28 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
DXMainClient/Resources/DTA/negotiation-failed.png New UI asset for failed negotiation indicator.
DXMainClient/Resources/DTA/negotiating.png New UI asset for negotiation-in-progress indicator.
DXMainClient/Domain/Multiplayer/PlayerInfo.cs Switches player ping to PingValue.
DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs Uses PingValue for LAN ping measurement.
DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs Exposes hosted LAN ping as PingValue.Unknown.
DXMainClient/Domain/Multiplayer/GenericHostedGame.cs Changes hosted-game ping abstraction to PingValue.
DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelCommunicator.cs Adds UDP communicator for V3 tunnel packets + handler dispatch.
DXMainClient/Domain/Multiplayer/CnCNet/V3PlayerNegotiator.cs Adds per-peer tunnel negotiation logic and messaging.
DXMainClient/Domain/Multiplayer/CnCNet/V3PlayerInfo.cs Adds per-player V3 negotiation state + tunnel scoring logic.
DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelBridge.cs Adds local game <-> tunnel forwarding bridge for V3.
DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs Supports tunnel versions 2+3, tunnel-failure eventing, V3 bridge start/stop, and ping-by-address deduping.
DXMainClient/Domain/Multiplayer/CnCNet/TunnelChosenEventArgs.cs Adds event args for negotiation outcomes.
DXMainClient/Domain/Multiplayer/CnCNet/PingValue.cs Introduces PingValue value type for ping/unknown handling + localization.
DXMainClient/Domain/Multiplayer/CnCNet/NegotiationDataManager.cs Tracks per-pair negotiation statuses/pings for UI.
DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs Uses PingValue and supports “dynamic tunnel” hosted-game representation.
DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs Stores ping as PingValue and updates ping-refresh behavior.
DXMainClient/DXGUI/Multiplayer/GameLobby/TunnelNegotiationStatusPanel.cs New negotiation status matrix panel UI.
DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs Adds negotiation textures and ping indicator support for negotiation states.
DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs Implements tunnel modes (V2/V3 static/V3 dynamic), STARTV2/STARTV3, negotiation broadcast/UX, and renegotiation on tunnel failure.
DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs Displays “dynamic” ping state and uses PingValue formatting.
DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs Adds target-version filtering and safer list scrolling behavior.
DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs Filters tunnels by version and updates ping display to PingValue.
DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs Integrates dynamic/legacy tunnel settings into game creation UX.
DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs Supports dynamic-tunnel broadcast parsing via [DYN].
DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs Updates ping messaging to use PingValue.
DXMainClient/DXGUI/Generic/OptionPanels/CnCNetOptionsPanel.cs Adds user settings toggles for legacy vs dynamic tunnels.
ClientCore/Settings/UserINISettings.cs Adds UseLegacyTunnels and UseDynamicTunnels INI settings.
ClientCore/ProgramConstants.cs Updates CnCNet protocol revision constant.
Comments suppressed due to low confidence (1)

DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs:91

  • V3TunnelCommunicator is initialized once and never shut down or refreshed: ConnectionManager_Disconnected just disables the component, and InitializeTunnelCommunicator() is a no-op after the first initialization. This leaves the UDP receive thread/socket running across disconnects, and it also prevents updating the tunnel endpoint map when the tunnel list refreshes. Consider adding a Stop/Dispose/Reset on disconnect and reinitializing (or updating endpoints) when the tunnel list changes.
        private void ConnectionManager_Connected(object sender, EventArgs e)
        {
            InitializeTunnelCommunicator();
            Enabled = true;
        }

        private void ConnectionManager_ConnectionLost(object sender, Online.EventArguments.ConnectionLostEventArgs e) => Enabled = false;

        private void ConnectionManager_Disconnected(object sender, EventArgs e) => Enabled = false;


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +9 to +11
public CnCNetTunnel ChosenTunnel { get; set; }
public bool IsLocalDecision { get; set; }
public string FailureReason { get; set; }
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TunnelChosenEventArgs defines PlayerName, ChosenTunnel, and FailureReason as non-nullable, but callers pass null for ChosenTunnel/FailureReason on failure and check e.ChosenTunnel != null downstream. Update the property types to be nullable (string?, CnCNetTunnel?) and/or provide safe defaults to avoid null-related runtime errors and clarify the API contract.

Suggested change
public CnCNetTunnel ChosenTunnel { get; set; }
public bool IsLocalDecision { get; set; }
public string FailureReason { get; set; }
public CnCNetTunnel? ChosenTunnel { get; set; }
public bool IsLocalDecision { get; set; }
public string? FailureReason { get; set; }

Copilot uses AI. Check for mistakes.
Comment on lines +1098 to +1099
NegotiationStatus? negotiationStatus,
string tooltipText)
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpdatePlayerPingIndicator(..., string tooltipText) treats tooltipText as optional (tooltipText ?? ...) and callers pass null, but the parameter type is non-nullable string. Mark it as string? (and ideally give it a default value) to match actual usage and avoid nullable warnings/contract confusion.

Suggested change
NegotiationStatus? negotiationStatus,
string tooltipText)
NegotiationStatus? negotiationStatus = null,
string? tooltipText = null)

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +77
_tunnelHandler = tunnelHandler;
_isDecider = localPlayer.Id < remotePlayer.Id;

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_isDecider is determined as localPlayer.Id < remotePlayer.Id, which makes the lower ID the decider. The PR description says the decider should be the player with the higher ID; either update the code to match the documented rule or update the documentation/spec so both clients agree on the same decider selection rule.

Copilot uses AI. Check for mistakes.
{
byte[] gameData = _localGameClient.Receive(ref remoteEndPoint);
_gameEndpoint = remoteEndPoint;

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BridgeWorker() reads receiverId from gameData.AsSpan(2) without validating packet length. If the local game sends a shorter UDP datagram (or a malformed one is received), ReadUInt16BigEndian will throw and terminate the bridge thread. Add a minimal length check before parsing the receiver ID (and ideally log+skip invalid packets).

Suggested change
// Validate that the packet is long enough to contain a receiver ID at offset 2
if (gameData.Length < 4)
{
Logger.Log($"V3GameTunnelBridge: Ignoring too-short game packet from local game (length={gameData.Length})");
continue;
}

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +194
HostedCnCNetGame hostedGame = (HostedCnCNetGame)game;
string pingText;

if (hostedGame.TunnelServer == null)
{
pingText = "Ping: Dynamic".L10N("Client:Main:GameInfoPingDynamic");
}
else
{
pingText = "Ping:".L10N("Client:Main:GameInfoPing") + " " + hostedGame.TunnelServer.Ping.ToString();
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetInfo(GenericHostedGame game) unconditionally casts game to HostedCnCNetGame. GameInformationPanel is used for both CnCNet and LAN games, so this will throw InvalidCastException for HostedLANGame (or any other GenericHostedGame implementation). Use pattern matching (game is HostedCnCNetGame hostedGame) and fall back to game.Ping for non-CnCNet games.

Suggested change
HostedCnCNetGame hostedGame = (HostedCnCNetGame)game;
string pingText;
if (hostedGame.TunnelServer == null)
{
pingText = "Ping: Dynamic".L10N("Client:Main:GameInfoPingDynamic");
}
else
{
pingText = "Ping:".L10N("Client:Main:GameInfoPing") + " " + hostedGame.TunnelServer.Ping.ToString();
string pingText;
if (game is HostedCnCNetGame hostedGame)
{
if (hostedGame.TunnelServer == null)
{
pingText = "Ping: Dynamic".L10N("Client:Main:GameInfoPingDynamic");
}
else
{
pingText = "Ping:".L10N("Client:Main:GameInfoPing") + " " + hostedGame.TunnelServer.Ping.ToString();
}
}
else
{
pingText = "Ping:".L10N("Client:Main:GameInfoPing") + " " + game.Ping.ToString();

Copilot uses AI. Check for mistakes.
new StringCommandHandler(NEGOTIATION_INFO_MESSAGE, HandleNegotiationInfoMessage),
new StringCommandHandler(TUNNEL_RENEGOTIATE_MESSAGE, HandleTunnelRenegotiateMessage),
new StringCommandHandler(TUNNEL_FAILED_MESSAGE, HandleTunnelFailedMessage),
new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage),
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctcpCommandHandlers registers CHANGE_TUNNEL_SERVER_MESSAGE twice. This will cause the tunnel change handler to run twice for every CHTNL message (potentially duplicating state changes / notices). Remove the duplicate registration.

Suggested change
new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage),

Copilot uses AI. Check for mistakes.
Comment on lines +2797 to +2805
CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort);
AddNotice($"{sender} is using tunnel: {tunnel.Name}");

if (!IsHost)
return;

var v3PlayerInfo = _v3PlayerInfos.FirstOrDefault(p => p.Name == sender);
if (v3PlayerInfo != null)
v3PlayerInfo.Tunnel = tunnel;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandlePlayerTunnelMessage assumes the tunnel exists in tunnelHandler.Tunnels and dereferences tunnel.Name without a null-check. If a client reports a tunnel that isn't present in the current tunnel list, this will throw. Handle the tunnel == null case (and avoid assigning a null tunnel to v3PlayerInfo.Tunnel).

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +102
PrintNegotiationResults();
_negotiationCompletionSource.TrySetResult(true);
NegotiationComplete?.Invoke(this, EventArgs.Empty);
return true;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NegotiateAsync() always returns true on the non-exception path and unconditionally TrySetResult(true) on _negotiationCompletionSource, even if the negotiation actually timed out or failed (e.g., non-decider timeout sets the TCS to false). This can cause the caller/UI to treat a failed negotiation as successful; consider awaiting the actual negotiation result (and for deciders, fail if no TunnelAck is received within the retry window).

Suggested change
PrintNegotiationResults();
_negotiationCompletionSource.TrySetResult(true);
NegotiationComplete?.Invoke(this, EventArgs.Empty);
return true;
// Await the actual negotiation result, which may be set to true or false
bool negotiationSucceeded = await _negotiationCompletionSource.Task;
PrintNegotiationResults();
NegotiationComplete?.Invoke(this, EventArgs.Empty);
return negotiationSucceeded;

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +159
var totalTunnels = _remotePlayer.TunnelResults.Count;
var completedTunnels = 0;
var selectionMade = false;
var completionLock = new object();
var selectionTcs = new TaskCompletionSource<bool>();

foreach (var kvp in _remotePlayer.TunnelResults)
{
var tunnel = kvp.Key;
var result = kvp.Value;
_ = WaitForTunnelResultsAsync(result, () => {
lock (completionLock)
{
completedTunnels++;
if (!selectionMade && completedTunnels >= Math.Max(2, totalTunnels * EARLY_SELECTION_THRESHOLD))
{
selectionMade = true;
selectionTcs.TrySetResult(true);
}
}
});
}

// Wait for early selection or all completion
await selectionTcs.Task;

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PerformDeciderNegotiationAsync() awaits selectionTcs.Task, but selectionTcs is only completed when completedTunnels >= Math.Max(2, totalTunnels * EARLY_SELECTION_THRESHOLD). If totalTunnels is 1 (or if totalTunnels * threshold rounds up above totalTunnels), selectionTcs is never set and negotiation hangs. Consider completing the TCS when completedTunnels == totalTunnels as well, and using Math.Max(1, ...) for the threshold.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +11
using DTAClient.DXGUI.Multiplayer.GameLobby;

#nullable enable

namespace DTAClient.Domain.Multiplayer.CnCNet;

/// <summary>
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NegotiationDataManager (Domain layer) depends on DTAClient.DXGUI.Multiplayer.GameLobby to get NegotiationStatus. This creates an inverted dependency (Domain -> UI) and will make it harder to reuse/test the domain code. Consider moving NegotiationStatus into a non-UI namespace (e.g., DTAClient.Domain.Multiplayer.CnCNet) so both UI and domain can reference it without a circular layering dependency.

Suggested change
using DTAClient.DXGUI.Multiplayer.GameLobby;
#nullable enable
namespace DTAClient.Domain.Multiplayer.CnCNet;
/// <summary>
#nullable enable
namespace DTAClient.Domain.Multiplayer.CnCNet;
/// <summary>
/// The status of connection/negotiation between two players.
/// </summary>
public enum NegotiationStatus
{
NotStarted = 0,
InProgress = 1,
Succeeded = 2,
Failed = 3
}
/// <summary>

Copilot uses AI. Check for mistakes.
Copy link
Member

@Rampastring Rampastring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the rest aside from CnCNetGameLobby.cs. Found stylistic issues to fix, and suggested a couple of optimizations to sending packets.

Copilot also seems to have found some valid issues.

public CnCNetTunnel? Tunnel { get; set; }
public V3PlayerNegotiator? Negotiator { get; set; }
public Dictionary<CnCNetTunnel, TunnelTestResult> TunnelResults { get; } = [];
private const int PACKET_LOSS_WEIGHT = 10;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constants in the client's code are usually put to the top of their class, before properties.

Leaving an empty line between constants and properties helps make them distinct, too.

public bool HasNegotiated { get; set; }
public bool IsNegotiating { get; set; }
public CnCNetTunnel? Tunnel { get; set; }
public V3PlayerNegotiator? Negotiator { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing the SetNegotiator method, I suggest either making the setter for this property private, or moving the code of SetNegotiator inside the setter for this property. As-is this has a public setter as a well as separate public set method, and from outside it might be confusing which should be used.

public bool StartNegotiation(V3PlayerInfo localPlayer, TunnelHandler tunnelHandler, List<CnCNetTunnel> availableTunnels)
{
if (this == localPlayer)
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended to happen? If not, maybe it should throw an exception instead to help catch bugs early?

Comment on lines +25 to +30
// If true, you send ping requests and measure latency.
// If false, you reply to ping requests
// This is set based on the ID (player1ID < player2ID)
// As a negotiator runs for each other player, you may be a decider for
// some, and a non-decider for others.
private readonly bool _isDecider;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These kinds of comments should be turned into summary comments so they show up when hovering over the variables elsewhere in code in Visual Studio. Also sentences should have proper punctuation (especially ending sentences with dots) to make them easier to read.

Suggested change
// If true, you send ping requests and measure latency.
// If false, you reply to ping requests
// This is set based on the ID (player1ID < player2ID)
// As a negotiator runs for each other player, you may be a decider for
// some, and a non-decider for others.
private readonly bool _isDecider;
/// <summary>
/// If true, you send ping requests and measure latency.
/// If false, you reply to ping requests.
///
/// This is set based on the ID (player1ID < player2ID).
/// As a negotiator runs for each other player, you may be a decider for
/// some, and a non-decider for others.
/// </summary>
private readonly bool _isDecider;

Comment on lines +134 to +138
var totalTunnels = _remotePlayer.TunnelResults.Count;
var completedTunnels = 0;
var selectionMade = false;
var completionLock = new object();
var selectionTcs = new TaskCompletionSource<bool>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Contributing.md:

Never use var with primitive types.

Suggested change
var totalTunnels = _remotePlayer.TunnelResults.Count;
var completedTunnels = 0;
var selectionMade = false;
var completionLock = new object();
var selectionTcs = new TaskCompletionSource<bool>();
int totalTunnels = _remotePlayer.TunnelResults.Count;
int completedTunnels = 0;
bool selectionMade = false;
var completionLock = new object();
var selectionTcs = new TaskCompletionSource<bool>();

Comment on lines +119 to +120
_handlers.TryRemove((localId, remoteId), out _);
Logger.Log($"V3TunnelCommunicator: Unregistered handler for {localId} <-> {remoteId}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this logs that the handler was removed even if TryRemove returned false. For more precise logging, could check the return value instead:

Suggested change
_handlers.TryRemove((localId, remoteId), out _);
Logger.Log($"V3TunnelCommunicator: Unregistered handler for {localId} <-> {remoteId}");
if (_handlers.TryRemove((localId, remoteId), out _))
Logger.Log($"V3TunnelCommunicator: Unregistered handler for {localId} <-> {remoteId}");
else
Logger.Log($"V3TunnelCommunicator: Handler not found for {localId} <-> {remoteId} while attempting unregistration");

Or, in case the handler should always be present when this is called, could reinforce it with an exception.

try
{
var packet = CreatePacket(senderId, receiverId, packetType, payload);
_udpClient!.Send(packet, packet.Length, tunnel.Address, tunnel.Port);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be optimized a little bit by changing the tunnel server's address into an IPEndPoint instance and caching it to the tunnel instance.

Then, you could use an overload of UdpClient.Send that takes an IPEndPoint instead of address + port, saving the system from having to parse the IP address string when sending each packet.

...looks like InitializeConnection already generates IPEndPoints for each tunnel, could just make use of them.

Another easy optimization would be changing the packet creation to write the packet to a Span allocated in the stack of this method, and then using the one of the UdpClient.Send overloads that take a ReadOnlySpan<Byte> parameter, avoiding the heap allocation of the packet byte array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2P & V3 tunnel support

6 participants