package net.minecraft.client.telemetry.events; import com.mojang.serialization.Codec; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import net.minecraft.client.Minecraft; import net.minecraft.client.telemetry.TelemetryEventType; import net.minecraft.client.telemetry.TelemetryProperty; import net.minecraft.client.telemetry.events.P2PTelemetryEvent.State.Snapshot; import net.minecraft.util.StringRepresentable; import org.jspecify.annotations.Nullable; public class P2PTelemetryEvent { public static final P2PTelemetryEvent INSTANCE = new P2PTelemetryEvent(); public void send( final boolean successful, final P2PTelemetryEvent.State state, @Nullable final Instant connectionStartTime, @Nullable final Instant signalingDoneTime, @Nullable final Instant connectionEstablishedTime ) { Snapshot snapshot = state.snapshot(); Long totalTimeMs = millisBetween(connectionStartTime, connectionEstablishedTime); Long signalingTimeMs = millisBetween(connectionStartTime, signalingDoneTime); Long iceConnectTimeMs = millisBetween(signalingDoneTime, connectionEstablishedTime); P2PTelemetryEvent.IcePath icePath = snapshot.localCandidateType() != null && snapshot.remoteCandidateType() != null ? P2PTelemetryEvent.IcePath.classify(snapshot.localCandidateType(), snapshot.remoteCandidateType()) : null; Minecraft.getInstance().getTelemetryManager().getOutsideSessionSender().send(TelemetryEventType.P2P_CONNECTION, properties -> { properties.put(TelemetryProperty.P2P_CONNECTION_SUCCESSFUL, successful); if (icePath != null) { properties.put(TelemetryProperty.P2P_CONNECTION_ICE_PATH, icePath); } if (snapshot.localCandidateType() != null) { properties.put(TelemetryProperty.P2P_CONNECTION_LOCAL_CANDIDATE_TYPE, snapshot.localCandidateType()); } if (snapshot.remoteCandidateType() != null) { properties.put(TelemetryProperty.P2P_CONNECTION_REMOTE_CANDIDATE_TYPE, snapshot.remoteCandidateType()); } if (totalTimeMs != null) { properties.put(TelemetryProperty.P2P_CONNECTION_TOTAL_TIME_MS, totalTimeMs); } if (signalingTimeMs != null) { properties.put(TelemetryProperty.P2P_CONNECTION_SIGNALING_TIME_MS, signalingTimeMs); } if (iceConnectTimeMs != null) { properties.put(TelemetryProperty.P2P_CONNECTION_ICE_CONNECT_TIME_MS, iceConnectTimeMs); } if (!successful && snapshot.failureStage() != null) { properties.put(TelemetryProperty.P2P_CONNECTION_FAILURE_STAGE, snapshot.failureStage()); } }); } @Nullable private static Long millisBetween(@Nullable final Instant from, @Nullable final Instant to) { return from != null && to != null ? from.until(to, ChronoUnit.MILLIS) : null; } public static enum FailureStage implements StringRepresentable { SIGNALING("SIGNALING"), ICE_CONNECT("ICE_CONNECT"), TIMEOUT("TIMEOUT"); public static final Codec CODEC = StringRepresentable.fromEnum(P2PTelemetryEvent.FailureStage::values); private final String name; private FailureStage(final String name) { this.name = name; } public String getSerializedName() { return this.name; } } public static enum IceCandidateType implements StringRepresentable { HOST("host"), SRFLX("srflx"), PRFLX("prflx"), RELAY("relay"); public static final Codec CODEC = StringRepresentable.fromEnum(P2PTelemetryEvent.IceCandidateType::values); private static final Map BY_NAME = (Map)Arrays.stream(values()) .collect(Collectors.toUnmodifiableMap(P2PTelemetryEvent.IceCandidateType::getSerializedName, Function.identity())); private final String name; private IceCandidateType(final String name) { this.name = name; } public String getSerializedName() { return this.name; } public static Optional byName(final String name) { return Optional.ofNullable((P2PTelemetryEvent.IceCandidateType)BY_NAME.get(name)); } } public static enum IcePath implements StringRepresentable { LOCAL("LOCAL"), DIRECT("DIRECT"), RELAY("RELAY"), UNKNOWN("UNKNOWN"); public static final Codec CODEC = StringRepresentable.fromEnum(P2PTelemetryEvent.IcePath::values); private final String name; private IcePath(final String name) { this.name = name; } public String getSerializedName() { return this.name; } public static P2PTelemetryEvent.IcePath classify(final P2PTelemetryEvent.IceCandidateType local, final P2PTelemetryEvent.IceCandidateType remote) { if (local == P2PTelemetryEvent.IceCandidateType.RELAY || remote == P2PTelemetryEvent.IceCandidateType.RELAY) { return RELAY; } else if (local == P2PTelemetryEvent.IceCandidateType.SRFLX || local == P2PTelemetryEvent.IceCandidateType.PRFLX || remote == P2PTelemetryEvent.IceCandidateType.SRFLX || remote == P2PTelemetryEvent.IceCandidateType.PRFLX) { return DIRECT; } else { return local == P2PTelemetryEvent.IceCandidateType.HOST && remote == P2PTelemetryEvent.IceCandidateType.HOST ? LOCAL : UNKNOWN; } } } public static final class State { @Nullable private P2PTelemetryEvent.IceCandidateType localCandidateType; @Nullable private P2PTelemetryEvent.IceCandidateType remoteCandidateType; @Nullable private P2PTelemetryEvent.FailureStage failureStage; public synchronized Snapshot snapshot() { return new Snapshot(this.localCandidateType, this.remoteCandidateType, this.failureStage); } public synchronized void setIceInfo(final P2PTelemetryEvent.IceCandidateType local, final P2PTelemetryEvent.IceCandidateType remote) { this.localCandidateType = local; this.remoteCandidateType = remote; } public synchronized void setFailureStage(final P2PTelemetryEvent.FailureStage failureStage) { if (this.failureStage == null) { this.failureStage = failureStage; } } } }