PretendoNetwork / nex-go

Barebones PRUDP/NEX server library written in Go
GNU Affero General Public License v3.0
74 stars 16 forks source link

[Enhancement]: Implement `TimeoutManager` and `Timeout` classes #63

Closed jonbarrow closed 5 months ago

jonbarrow commented 6 months ago

Checked Existing

What enhancement would you like to see?

Implement the TimeoutManager and Timeout classes to replace the current timeout/scheduler mechanism. This directly relates to both https://github.com/PretendoNetwork/nex-go/issues/49 and https://github.com/PretendoNetwork/nex-go/pull/60 (though this should probably be implemented outside of that PR).

We currently do not implement timeouts and packet retransmission the way NEX/QRV does at all, and it's half-baked at best. For more accurate emulation we should actually implement the relevant classes and use them.

Depending on how we approach this, this is not a trivial thing to implement, as it would require several other structural changes. But I'm not 100% sure we need those changes right now? Some of this information was already went over here https://github.com/PretendoNetwork/nex-go/pull/58#issuecomment-2132365836, but I'll restate it here as well.

Decomp (cleaned and modified for clarity):

void nn::nex::PRUDPEndPoint::SendReliable(nn::nex::PRUDPEndPoint *this, nn::nex::PacketOut *packet) {
    uint uVar1;
    uint uVar2;
    nn::nex::SlidingWindow *sliding_window;
    bool data_pending;
    undefined4 retransmit_timeout;
    nn::nex::StreamSettings *stream_settings;
    uchar destination_stream_id;
    nn::nex::Key *key;
    ushort new_resend_count;
    ushort payload_size;
    int substream_id;
    int send_result;
    undefined8 current_time;

    // packet->type_flags & 0xF checks the packet type, type 3 is DISCONNECT
    if (((packet->type_flags & 0xF) == 3) && (substream_id = 0, this->max_substreams != 0xFFFFFFFF)) {
        do {
            // Loop through all SlidingWindows and if any still have data, bail the send
            sliding_window = (nn::nex::SlidingWindow *)__CPR218____vc__Q2_3std100vector__tm__86_PQ3_2nn3nex13SlidingWindowQ3_2nn3nex47MemAllocator__tm__27_PQ3_2nn3nexJ42JFQ4_3std25_Vector_val__tm__7_Z1ZZ2Z5_Alty9size_type_QJ118JdJ124JJ151J9reference(&this->substreams, substream_id);
            data_pending = nn::nex::SlidingWindow::DataPending(*(nn::nex::SlidingWindow **)sliding_window);

            if (data_pending) {
                this->field664_0x2b8 = this->field664_0x2b8 + 1;
                nn::nex::TimeoutManager::SchedulePacketTimeout(&this->prudp_stream->timeout_manager, packet);
                return;
            }

            substream_id = substream_id + 1;
        } while (substream_id < (int)(this->max_substreams + 1));
    }

    // Increase resend count
    new_resend_count = packet->resend_count + 1;
    uVar2 = this->field760_0x324;
    uVar1 = (uint)new_resend_count;
    packet->resend_count = new_resend_count;

    if (uVar2 < uVar1) {
        this->field760_0x324 = uVar1;
    }

    if (1 < uVar1) {
        this->field758_0x31c = this->field758_0x31c + 1;
        payload_size = nn::nex::Packet::GetPayloadSize((nn::nex::Packet *)packet);
        this->field759_0x320 = this->field759_0x320 + (uint)payload_size;
    }

    // Setup the packets timeout timer and schedule it
    retransmit_timeout = nn::nex::PRUDPEndPoint::ComputeRetransmitTimeout(this, packet);
    nn::nex::Timeout::SetRTO(packet->timeout, retransmit_timeout);
    nn::nex::TimeoutManager::SchedulePacketTimeout(&this->prudp_stream->timeout_manager, packet);
    stream_settings = (nn::nex::StreamSettings *)GetValue__Q3_2nn3nex55PseudoGlobalVariable__tm__27_Q3_2nn3nex14StreamSettingsFv_RZ1Z(&DAT_104bd2a8 + this->prudp_stream->stream_type * 0x200);

    // Track the last time a packet was sent?
    if (stream_settings->field_0x90 != '\0') {
        current_time = nn::nex::Time::GetTime();
        this->last_send_time = current_time;
    }

    // Send the packet
    destination_stream_id = nn::nex::StationURL::GetStreamID(&this->station_url);
    key = (nn::nex::Key *)nn::nex::PacketEncDec::GetEncryptionKey(&this->crypto);
    send_result = nn::nex::PRUDPStream::Send(this->prudp_stream,this->port, destination_stream_id, packet, key);

    if (send_result == 0) {
        this->field757_0x318 = this->field757_0x318 + 1;
    }

    return;
}

SchedulePacketTimeout follows the following logic:

Decomp (cleaned and modified for clarity):

void nn::nex::TimeoutManager::SchedulePacketTimeout(nn::nex::TimeoutManager *this, nn::nex::PacketOut *packet_out) {
    uint uVar1;
    code *pcVar2;
    int network_lock;
    int found_packet;
    int current_time;
    undefined4 extraout_r4;
    nn::nex::PacketOut *pnVar5;
    ushort packet_type;
    nn::nex::PacketOut *packet;
    undefined4 begin_times;
    undefined4 search_packets;
    undefined4 end_packets;
    undefined4 end_times;
    undefined4 local_44;
    undefined8 awaited_time;
    undefined4 local_28;
    undefined4 local_24;
    undefined auStack_20 [20];

    packet = packet_out;
    network_lock = GetLock__Q3_2nn3nex7NetworkSFv();

    if ((*(int *)(network_lock + 0x4c) == 2) ||
         (((*(int *)(network_lock + 0x4c) == 1 && (uVar1 = *(uint *)(network_lock + 0x48), uVar1 != 0)) &&
            ((DAT_101ac364 & uVar1) == uVar1)))) {
        nn::nex::CriticalSection::EnterImpl(network_lock);
    }

    if (
        (this->quick_timeout != false) && // Are quick times enabled
        (
            (
                // Check if packet type is 0, 1, or 3 (SYN, CONNECT, or DISCONNECT)
                (packet_type = packet->type_flags & 0xF, (packet->type_flags & 0xF) == 0 || (packet_type == 1)) ||
                (packet_type == 3)
            )
        )
    ) {
        nn::nex::Timeout::SetRTO(packet->timeout, 1); // Overwrite the packets timeout with a value of 1
        local_28 = 0;
        local_24 = extraout_r4;
        nn::nex::Timeout::SetExpirationTime(packet->timeout, &local_28); // Set the expiration time to 0?
    }

    // Start the timeout
    nn::nex::Timeout::Start(packet->timeout);

    // The following checks to see if old data exists
    // and removes it if so

    // See https://en.cppreference.com/w/cpp/container/set/find
    // this.packets is an std::set of nn::nex::PacketOut*
    search_packets = __CPR242__find__Q2_3std173_Tree__tm__159_Q2_3std148_Tset_traits__tm__127_PQ3_2nn3nex9PacketOutQ2_3std34less__tm__22_PQ3_2nn3nexJ74JQ3_2nn3nex42MemAllocator__tm__J103JXCbL_1_0FRCQ2_Z1Z8key_type_Q3_3std16_Tree__tm__4_Z1Z8iterator(&this->packets, &packet);
    end_packets = __CPR225__end__Q2_3std173_Tree__tm__159_Q2_3std148_Tset_traits__tm__127_PQ3_2nn3nex9PacketOutQ2_3std34less__tm__22_PQ3_2nn3nexJ73JQ3_2nn3nex42MemAllocator__tm__J102JXCbL_1_0Fv_Q3_3std16_Tree__tm__4_Z1Z8iterator(&this->packets);
    found_packet = __CPR252____ne__Q3_3std173_Tree__tm__159_Q2_3std148_Tset_traits__tm__127_PQ3_2nn3nex9PacketOutQ2_3std34less__tm__22_PQ3_2nn3nexJ74JQ3_2nn3nex42MemAllocator__tm__J103JXCbL_1_014const_iteratorCFRCQ3_3std16_Tree__tm__4_Z1Z14const_iterator_b(&search_packets, &end_packets);

    // Packet was not in the set
    if (found_packet != 0) {
        // See https://en.cppreference.com/w/cpp/container/multimap/find
        // this.times is an std::multimap where the key is nn::nex::Time (not pointer) and value is nn::nex::PacketOut*
        begin_times = begin__Q2_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_1Fv_Q3_3std16_Tree__tm__4_Z1Z8iterator(&this->times);
        end_times = end__Q2_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_1Fv_Q3_3std16_Tree__tm__4_Z1Z8iterator(&this->times);
        current_time = __ne__Q3_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_114const_iteratorCFRCQ3_3std16_Tree__tm__4_Z1Z14const_iterator_b(&begin, &end_times);

        while (current_time != 0) {
            current_time = __rf__Q3_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_18iteratorCFv_Q2_Z1Z6_ITptr(&begin);

            // std::multimap is made of nodes with 2 members, "first" and "second"
            // "first" is the key
            // "second" is the value
            // "current_time + 8" pulls the "second" member, the value
            if (*(nn::nex::PacketOut **)(current_time + 8) == packet) {
                local_44 = begin_times;

                // Remove the entries from both the map and set?
                erase__Q2_3std183multimap__tm__166_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutFQ3_3std64_Tree__tm__51_Q2_3std41_Tmap_traits__tm__21_Z1ZZ2ZZ3ZZ4ZXCbL_1_18iterator_v(&this->times, &local_44);
                __CPR235__erase__Q2_3std131set__tm__119_PQ3_2nn3nex9PacketOutQ2_3std34less__tm__22_PQ3_2nn3nexJ41JQ3_2nn3nex42MemAllocator__tm__J70JFRCZ1Z_Q3_3std61_Tree__tm__48_Q2_3std38_Tset_traits__tm__18_Z1ZZ2ZZ3ZXCbL_1_09size_type(&this->packets, &packet);

                // Not sure what this actually is.
                // Variable reused
                pnVar5 = packet;
                current_time = nn::nex::AtomicValue::IncAndGet(&packet->field_0x4, 0xFFFFFFFF);

                if ((current_time == 0) && (pnVar5->field_0x8 == '\0')) {
                    pcVar2 = *(code **)(*(int *)&pnVar5->field_0xc + 0xc);
                    pnVar5->field_0x8 = 1;
                    (*pcVar2)(pnVar5, 3);
                    __CPR270__insert__Q2_3std173_Tree__tm__159_Q2_3std148_Tset_traits__tm__127_PQ3_2nn3nex9PacketOutQ2_3std34less__tm__22_PQ3_2nn3nexJ76JQ3_2nn3nex42MemAllocator__tm__J105JXCbL_1_0FRCQ2_Z1Z10value_type_Q2_3std48pair__tm__36_Q3_3std16_Tree__tm__4_Z1Z8iteratorb(&this->packets, &packet);

                    awaited_time = nn::nex::Timeout::GetAwaitedTime(packet->timeout);
                    __ct__Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutFRCZ1ZRCZ2Z(auStack_20,&awaited_time,&packet);
                    __CPR372__insert__Q2_3std183multimap__tm__166_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nexJ46JQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nexJ46JPQ3_2nn3nexJ62JFRCQ3_3std64_Tree__tm__51_Q2_3std41_Tmap_traits__tm__21_Z1ZZ2ZZ3ZZ4ZXCbL_1_110value_type_Q3_3stdJ211J8iterator(&this->times, auStack_20);
                    OSAddAtomic(&packet->field_0x4,1);

                    current_time = *(int *)(network_lock + 0x4c);
                    goto joined_r0x02e864b4;
                }

                break;
            }

            // Increment the "begin_times" pointer and get the next node
            __pp__Q3_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_18iteratorFv_RQ3_3std16_Tree__tm__4_Z1Z8iterator(&begin_times);

            end_times = end__Q2_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_1Fv_Q3_3std16_Tree__tm__4_Z1Z8iterator(&this->times);
            current_time = __ne__Q3_3std220_Tree__tm__206_Q2_3std195_Tmap_traits__tm__174_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nex4TimeQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutXCbL_1_114const_iteratorCFRCQ3_3std16_Tree__tm__4_Z1Z14const_iterator_b(&begin_times, &end_times);
        }
    }

    // Insert the packet and time into the map and set

    __CPR270__insert__Q2_3std173_Tree__tm__159_Q2_3std148_Tset_traits__tm__127_PQ3_2nn3nex9PacketOutQ2_3std34less__tm__22_PQ3_2nn3nexJ76JQ3_2nn3nex42MemAllocator__tm__J105JXCbL_1_0FRCQ2_Z1Z10value_type_Q2_3std48pair__tm__36_Q3_3std16_Tree__tm__4_Z1Z8iteratorb(&this->packets, &packet);

    awaited_time = nn::nex::Timeout::GetAwaitedTime(packet->timeout);

    __ct__Q2_3std50pair__tm__38_CQ3_2nn3nex4TimePQ3_2nn3nex9PacketOutFRCZ1ZRCZ2Z(auStack_20, &awaited_time, &packet);
    __CPR372__insert__Q2_3std183multimap__tm__166_Q3_2nn3nex4TimePQ3_2nn3nex9PacketOutQ2_3std28less__tm__16_Q3_2nn3nexJ46JQ3_2nn3nex80MemAllocator__tm__60_Q2_3std50pair__tm__38_CQ3_2nn3nexJ46JPQ3_2nn3nexJ62JFRCQ3_3std64_Tree__tm__51_Q2_3std41_Tmap_traits__tm__21_Z1ZZ2ZZ3ZZ4ZXCbL_1_110value_type_Q3_3stdJ211J8iterator(&this->times, auStack_20);
    OSAddAtomic(&packet->field_0x4, 1);

    // Not sure what this actually is.
    // Variable reused
    found_packet = *(int *)(network_lock + 0x4c);
joined_r0x02e864b4:
    if (found_packet != 2) {
        if (*(int *)(network_lock + 0x4c) != 1) {
            return;
        }

        uVar1 = *(uint *)(network_lock + 0x48);

        if (uVar1 == 0) {
            return;
        }

        if ((DAT_101ac364 & uVar1) != uVar1) {
            return;
        }
    }

    nn::nex::CriticalSection::LeaveImpl(network_lock);

    return;
}

The actual timeout handling is handled by 3 different methods:

This can easily be reimplemented using contexts, like shown in https://github.com/PretendoNetwork/nex-go/issues/49. However we have to decide on how to structure this, and where to store some of these things. We do not currently have a PRUDPStream implementation, which means we can't store the TimeoutManager there?

This also means we should probably bring back SlidingWindow for sending packets.

Any other details to share? (OPTIONAL)

All information is coming from Xenoblade on the Wii U, though it should be applicable to all other NEX/RDV titles. I just find the implementations in Xenoblade to be more readable

jonbarrow commented 6 months ago

Forgot to include this in the original breakdown, adding here. This is the decomp of ComputeRetransmitTimeout (cleaned and modified for clarity):

uint nn::nex::PRUDPEndPoint::ComputeRetransmitTimeout(nn::nex::PRUDPEndPoint *this, nn::nex::PacketOut *packet) {
    nn::nex::StreamSettings *stream_settings;
    int rtt_avg;
    int rtt_dev;
    uint uVar1;
    uint uVar2;
    code *pcVar3;
    double dVar4;
    double extraout_f1;
    double extraout_f1_00;
    double dVar5;

    stream_settings = (nn::nex::StreamSettings *)GetValue__Q3_2nn3nex55PseudoGlobalVariable__tm__27_Q3_2nn3nex14StreamSettingsFv_RZ1Z(&DAT_104bd2a8 + this->prudp_stream->stream_type * 0x200);

    if (DAT_101ac494 != (code *)0x0) {
        pcVar3 = DAT_101ac494;
        rtt_avg = nn::nex::RTT::GetRTTSmoothedAvg(&this->field_0xa4);
        rtt_dev = nn::nex::RTT::GetRTTSmoothedDev(&this->field_0xa4);
        uVar1 = (*pcVar3)(rtt_avg + rtt_dev * 4,packet->resend_count);
        return uVar1;
    }

    if ((packet->type_flags & 0xf) == 0) {
        dVar5 = extraout_f1;
        rtt_avg = nn::nex::StreamSettings::GetSynInitialRTT(stream_settings);
        dVar4 = (double)(float)((double)CONCAT44(0x43300000,rtt_avg * (uint)packet->resend_count) - 4503599627370496.0);
        nn::nex::PRUDPEndPoint::GetTimeoutMultiplier(this, packet->resend_count, stream_settings);
        dVar4 = dVar4 * dVar5;
    } else {
        rtt_avg = nn::nex::RTT::GetRTTSmoothedAvg(&this->field_0xa4);
        rtt_dev = nn::nex::RTT::GetRTTSmoothedDev(&this->field_0xa4);
        dVar4 = (double)(float)((double)CONCAT44(0x43300000, (rtt_avg + rtt_dev * 4) * (uint)packet->resend_count) - 4503599627370496.0);
        dVar5 = extraout_f1_00;
        nn::nex::PRUDPEndPoint::GetTimeoutMultiplier(this, packet->resend_count, stream_settings);
        dVar4 = dVar4 * dVar5;
    }

    if (2.147484e+09 <= (float)dVar4) {
        uVar1 = (int)((float)dVar4 - 2.147484e+09) + 0x80000000;
        uVar2 = stream_settings->field151_0xb8;

        if (uVar1 <= uVar2) {
            return uVar1;
        }
    } else {
        uVar2 = stream_settings->field151_0xb8;

        if ((uint)(int)dVar4 <= uVar2) {
            return (int)dVar4;
        }
    }

    return uVar2;
}

It bases the timeout time based on the connections RTT, packet type, and a multiplier. The multiplier is determined like so:

void nn::nex::PRUDPEndPoint::GetTimeoutMultiplier(nn::nex::PRUDPEndPoint *this, ushort resend_count, nn::nex::StreamSettings *stream_settings) {
    uint uVar1;
    undefined2 in_register_00000010;
    uint uVar2;

    uVar2 = CONCAT22(in_register_00000010, resend_count);
    uVar1 = nn::nex::StreamSettings::GetExtraRetransmitTimeoutTrigger(stream_settings);

    if (uVar2 < uVar1) {
        nn::nex::StreamSettings::GetRetransmitTimeoutMultiplier(stream_settings);

        return;
    }

    nn::nex::StreamSettings::GetExtraRetransmitTimeoutMultiplier(stream_settings);

    return;
}

Obviously this decomp isn't perfect, since GetTimeoutMultiplier is returning void right now. But it's a good start.

The CONCATXY functions are specific to Ghidra. CONCATXY takes in 2 values and concats their bytes, where X and Y are the sizes in bytes of the 2 numbers. So for example a call like CONCAT43(0x43300000, 0xFFFFF) would do (0x43300000 << 4*8) | 0xFFFFF and produce 0x43300000000FFFFF.

jonbarrow commented 5 months ago

I've found what DAT_101ac494 is in ComputeRetransmitTimeout. It's an optional function for calculating the timeout. This is likely provided as a way for developers to define their own handling here. It's definied in nn::nex::PRUDPEndPoint::SetCalcRetransmissionTimeoutCallback, and takes in 2 values:

It then returns the value from this function as the timeout.

Since this is likely something set per-game, as an option, we likely don't need to implement it ourselves. Just the same interface for setting the custom handler.

jonbarrow commented 5 months ago

I have just checked the implementation in WATCH_DOGS and SendReliable is handled slightly differently here, though some of it makes sense. In WATCH_DOGS only a single SlidingWindow is checked before sending the packet, though this would make sense if it uses packets pre-PRUDPv1.

The weird part about the check is that it checks for the client's connection state to be anything besides 6? We only know of states 0-4, I have no idea what this state is and Xenoblade doesn't make it.

WATCH_DOGS shows that the uVar1 (new_resend_count) checks in Xenoblade seem to be just debug statistics, which we can likely ignore.

There also is no implementation of ComputeRetransmitTimeout here, WATCH_DOGS instead in-lines the calculations entirely, though the calculations are mostly the same it seems?

There is an additional check for a field on the PRUDPEndPoint, however I believe we can safely ignore this as well. All it does is set the multiplier to 1 if the value is false, however it's always set to true and no code updates it. It's possible this is also just a debug feature.

Decomp (modified and cleaned for clarity):

void rdv::PRUDPEndPoint::SendReliable(rdv::PRUDPEndPoint *this, rdv::PacketOut *packet) {
    bool more_pending;
    rdv::StreamSettings *stream_settings;
    rdv::Timeout *packet_timeout;
    uint extra_retransmit_timeout_trigger;
    uint stream_id;
    uint packet_size;
    ushort new_resend_count;
    uint resend_count;
    int retransmit_time_base;
    uint retransmit_time_base_multiplier;
    double retransmit_multiplier;
    int rto;

    // If packet is DISCONNECT and client is not in state 6 and has more packets to send
    if (
            (
                ((packet->flags_and_type & 7) == 3) && (this->connection_state != 6)
            ) && (more_pending = rdv::SlidingWindow::DataPending(this->sliding_window), more_pending)
    ) {
        rdv::TimeoutManager::SchedulePacketTimeout(&this->prudp_stream->timeout_manager, packet);
        return;
    }

    // Increase the packets resend count
    new_resend_count = packet->resend_count + 1;
    resend_count = (uint)new_resend_count;
    packet->resend_count = new_resend_count;
    retransmit_time_base_multiplier = resend_count;

    stream_settings = rdv::Stream::GetSettings((rdv::Stream *)this->prudp_stream);

    // Likely some debug setting? This is always
    // set to true in WATCH_DOGS, and is never
    // updated. Haven't found a use for this
    if (stream_settings->field39_0x4e == false) {
        retransmit_time_base_multiplier = 1;
    }

    // this->rtt is updated by rdv::RTT::Adjust(this->rtt, new_value)
    // inside both rdv::PRUDPEndPoint::PacketAcknowledged()
    // and rdv::PRUDPEndPoint::ServiceIncomingPacket()
    // this->rtt refers to the first field of the rdv::RTT which is
    // the estimated RTT
    //
    // I have no idea what this->field103_0x80 is. It seems to never be
    // referenced outside of this function, even in the rdv::PRUDPEndPoint
    // constructor function
    retransmit_time_base = (this->rtt >> 3) + (this->field103_0x80 & 0xFFFFFFFCU);
    extra_retransmit_timeout_trigger = rdv::StreamSettings::GetExtraRetransmitTimeoutTrigger(stream_settings);

    // Calculate the multiplier
    if (resend_count < extra_retransmit_timeout_trigger) {
        retransmit_multiplier = (double)rdv::StreamSettings::GetRetransmitTimeoutMultiplier(stream_settings);
        retransmit_multiplier = (double)(float)((double)CONCAT44(0x43300000, retransmit_time_base * retransmit_time_base_multiplier) -4503599627370496.0) * retransmit_multiplier;
    } else {
        retransmit_multiplier = (double)rdv::StreamSettings::GetExtraRetransmitTimeoutMultiplier(stream_settings);
        retransmit_multiplier = (double)(float)((double)CONCAT44(0x43300000, retransmit_time_base * retransmit_time_base_multiplier) -4503599627370496.0) * retransmit_multiplier;
    }

    // Prevent the multiplier from overflowing
    // the uint32 limit
    if (2.147484e+09 <= (float)retransmit_multiplier) {
        rto = (int)((float)retransmit_multiplier - 2.147484e+09) + 0x80000000;
    } else {
        rto = (int)retransmit_multiplier;
    }

    // Packet has been resent more than once
    if (1 < resend_count) {
        rdv::TransportPerfCounters::Inc(this->prudp_stream->field12_0xc + 0x10, 7, 1);
        rdv::TransportPerfCounters::Inc(&this->field161_0xc8, 7, 1);
    }

    // Update the RTO, start the timeout, and send the packet
    packet_timeout = rdv::PacketOut::GetTimeout(packet);

    rdv::Timeout::SetRTO(packet_timeout, rto);
    rdv::TimeoutManager::SchedulePacketTimeout(&this->prudp_stream->timeout_manager, packet);

    stream_id = rdv::StationURL::GetStreamID(&this->station_url);

    rdv::PRUDPStream::Send(this->prudp_stream, this->field74_0x50, (uchar)stream_id, packet);

    // If packet is DATA. Likely some debug stats? Haven't found a use for this
    if ((packet->flags_and_type & 7) == 2) {
        packet_size = rdv::Packet::GetSize(packet);
        rdv::TransportPerfCounters::Inc(&this->field161_0xc8, 0, packet_size);
    }

    return;
}

Given that they have different methods of calculating the RTO, I'd say it's safe to assume it doesn't matter THAT badly how we do it? We should just pick an implementation and stick with it. I think the WATCH_DOGS implementation is the simpler one, but if we want to be closer to NEX then we should use the Xenoblade implementation. Regardless of implementation though I believe we should still use the SetCalcRetransmissionTimeoutCallback method so games can define this entirely themselves if they wish.

rdv::RTT::Adjust() looks like (modified and cleaned for clarity):

void rdv::RTT::Adjust(rdv::RTT *this, int new_rtt) {
    int difference;

     // Calculate the difference between the new RTT measurement
     // and 1/8 of the current estimated RTT
    difference = new_rtt - ((uint)this->estimated >> 3);

    // Always positive
    if (difference < 0) {
        difference = -difference;
    }

    // Update the estimation, last RTT, and variance
    this->estimated = this->estimated + difference;
    this->last = new_rtt;
    this->variance = this->variance + (difference - ((uint)this->variance >> 2));

    return;
}