diff --git a/src/com/hypixel/hytale/builtin/adventure/teleporter/TeleporterPlugin.java b/src/com/hypixel/hytale/builtin/adventure/teleporter/TeleporterPlugin.java index e86498b..1c2a55d 100644 --- a/src/com/hypixel/hytale/builtin/adventure/teleporter/TeleporterPlugin.java +++ b/src/com/hypixel/hytale/builtin/adventure/teleporter/TeleporterPlugin.java @@ -2,7 +2,9 @@ package com.hypixel.hytale.builtin.adventure.teleporter; import com.hypixel.hytale.builtin.adventure.teleporter.component.Teleporter; import com.hypixel.hytale.builtin.adventure.teleporter.interaction.server.TeleporterInteraction; +import com.hypixel.hytale.builtin.adventure.teleporter.interaction.server.UsedTeleporter; import com.hypixel.hytale.builtin.adventure.teleporter.page.TeleporterSettingsPageSupplier; +import com.hypixel.hytale.builtin.adventure.teleporter.system.ClearUsedTeleporterSystem; import com.hypixel.hytale.builtin.adventure.teleporter.system.CreateWarpWhenTeleporterPlacedSystem; import com.hypixel.hytale.builtin.teleport.TeleportPlugin; import com.hypixel.hytale.component.AddReason; @@ -19,12 +21,14 @@ import com.hypixel.hytale.server.core.modules.interaction.interaction.config.ser import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class TeleporterPlugin extends JavaPlugin { private static TeleporterPlugin instance; private ComponentType teleporterComponentType; + private ComponentType usedTeleporterComponentType; public static TeleporterPlugin get() { return instance; @@ -41,6 +45,8 @@ public class TeleporterPlugin extends JavaPlugin { this.getChunkStoreRegistry().registerSystem(new TeleporterPlugin.TeleporterOwnedWarpRefChangeSystem()); this.getChunkStoreRegistry().registerSystem(new TeleporterPlugin.TeleporterOwnedWarpRefSystem()); this.getChunkStoreRegistry().registerSystem(new CreateWarpWhenTeleporterPlacedSystem()); + this.usedTeleporterComponentType = this.getEntityStoreRegistry().registerComponent(UsedTeleporter.class, UsedTeleporter::new); + this.getEntityStoreRegistry().registerSystem(new ClearUsedTeleporterSystem()); this.getCodecRegistry(Interaction.CODEC).register("Teleporter", TeleporterInteraction.class, TeleporterInteraction.CODEC); this.getCodecRegistry(OpenCustomUIInteraction.PAGE_CODEC) .register("Teleporter", TeleporterSettingsPageSupplier.class, TeleporterSettingsPageSupplier.CODEC); @@ -50,6 +56,10 @@ public class TeleporterPlugin extends JavaPlugin { return this.teleporterComponentType; } + public ComponentType getUsedTeleporterComponentType() { + return this.usedTeleporterComponentType; + } + private static class TeleporterOwnedWarpRefChangeSystem extends RefChangeSystem { @Nonnull @Override diff --git a/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/TeleporterInteraction.java b/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/TeleporterInteraction.java index 88df5cd..0b664dd 100644 --- a/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/TeleporterInteraction.java +++ b/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/TeleporterInteraction.java @@ -4,6 +4,7 @@ import com.hypixel.hytale.builtin.adventure.teleporter.component.Teleporter; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.validation.Validators; import com.hypixel.hytale.component.Archetype; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Ref; @@ -49,10 +50,30 @@ public class TeleporterInteraction extends SimpleBlockInteraction { ) .documentation("The particle to play on the entity when teleporting.") .add() + .appendInherited( + new KeyedCodec<>("ClearOutXZ", Codec.DOUBLE), + (interaction, s) -> interaction.clearoutXZ = s, + interaction -> interaction.clearoutXZ, + (interaction, parent) -> interaction.clearoutXZ = parent.clearoutXZ + ) + .addValidator(Validators.greaterThanOrEqual(0.0)) + .documentation("Upon reaching the warp destination, how far away one has to move on the XZ plane in order to use another Teleporter.") + .add() + .appendInherited( + new KeyedCodec<>("ClearOutY", Codec.DOUBLE), + (interaction, s) -> interaction.clearoutY = s, + interaction -> interaction.clearoutY, + (interaction, parent) -> interaction.clearoutY = parent.clearoutY + ) + .addValidator(Validators.greaterThanOrEqual(0.0)) + .documentation("Upon reaching the warp destination, how far away one has to move along the Y axis in order to use another Teleporter.") + .add() .build(); - private static final Duration TELEPORT_GLOBAL_COOLDOWN = Duration.ofMillis(250L); + private static final Duration TELEPORTER_GLOBAL_COOLDOWN = Duration.ofMillis(100L); @Nullable private String particle; + private double clearoutXZ = 1.3; + private double clearoutY = 2.5; @Nonnull @Override @@ -88,38 +109,49 @@ public class TeleporterInteraction extends SimpleBlockInteraction { if (playerComponent == null || !playerComponent.isWaitingForClientReady()) { Archetype archetype = commandBuffer.getArchetype(ref); if (!archetype.contains(Teleport.getComponentType()) && !archetype.contains(PendingTeleport.getComponentType())) { - WorldChunk worldChunkComponent = chunkRef.getStore().getComponent(chunkRef, WorldChunk.getComponentType()); + if (!archetype.contains(UsedTeleporter.getComponentType())) { + WorldChunk worldChunkComponent = chunkRef.getStore().getComponent(chunkRef, WorldChunk.getComponentType()); + if (worldChunkComponent != null) { + BlockType blockType = worldChunkComponent.getBlockType(targetBlock.x, targetBlock.y, targetBlock.z); + if (blockType != null) { + if (!teleporter.isValid()) { + String currentState = blockType.getStateForBlock(blockType); + if (!"default".equals(currentState)) { + BlockType variantBlockType = blockType.getBlockForState("default"); + if (variantBlockType != null) { + worldChunkComponent.setBlockInteractionState( + targetBlock.x, targetBlock.y, targetBlock.z, variantBlockType, "default", true + ); + } + } + } - assert worldChunkComponent != null; - - BlockType blockType = worldChunkComponent.getBlockType(targetBlock.x, targetBlock.y, targetBlock.z); - if (!teleporter.isValid()) { - String currentState = blockType.getStateForBlock(blockType); - if (!"default".equals(currentState)) { - BlockType variantBlockType = blockType.getBlockForState("default"); - if (variantBlockType != null) { - worldChunkComponent.setBlockInteractionState(targetBlock.x, targetBlock.y, targetBlock.z, variantBlockType, "default", true); - } - } - } - - TransformComponent transformComponent = commandBuffer.getComponent(ref, TransformComponent.getComponentType()); - - assert transformComponent != null; - - Teleport teleportComponent = teleporter.toTeleport(transformComponent.getPosition(), transformComponent.getRotation(), targetBlock); - if (teleportComponent != null) { - TeleportRecord recorder = commandBuffer.getComponent(ref, TeleportRecord.getComponentType()); - if (recorder == null || recorder.hasElapsedSinceLastTeleport(TELEPORT_GLOBAL_COOLDOWN)) { - commandBuffer.addComponent(ref, Teleport.getComponentType(), teleportComponent); - if (this.particle != null) { - Vector3d particlePosition = transformComponent.getPosition(); - SpatialResource, EntityStore> playerSpatialResource = commandBuffer.getResource( - EntityModule.get().getPlayerSpatialResourceType() - ); - ObjectList> results = SpatialResource.getThreadLocalReferenceList(); - playerSpatialResource.getSpatialStructure().collect(particlePosition, 75.0, results); - ParticleUtil.spawnParticleEffect(this.particle, particlePosition, results, commandBuffer); + TransformComponent transformComponent = commandBuffer.getComponent(ref, TransformComponent.getComponentType()); + if (transformComponent != null) { + Teleport teleportComponent = teleporter.toTeleport( + transformComponent.getPosition(), transformComponent.getRotation(), targetBlock + ); + if (teleportComponent != null) { + TeleportRecord recorder = commandBuffer.getComponent(ref, TeleportRecord.getComponentType()); + if (recorder == null || recorder.hasElapsedSinceLastTeleport(TELEPORTER_GLOBAL_COOLDOWN)) { + commandBuffer.addComponent(ref, Teleport.getComponentType(), teleportComponent); + commandBuffer.addComponent( + ref, + UsedTeleporter.getComponentType(), + new UsedTeleporter(teleporter.getWorldUuid(), teleportComponent.getPosition(), this.clearoutXZ, this.clearoutY) + ); + if (this.particle != null) { + Vector3d particlePosition = transformComponent.getPosition(); + SpatialResource, EntityStore> playerSpatialResource = commandBuffer.getResource( + EntityModule.get().getPlayerSpatialResourceType() + ); + ObjectList> results = SpatialResource.getThreadLocalReferenceList(); + playerSpatialResource.getSpatialStructure().collect(particlePosition, 75.0, results); + ParticleUtil.spawnParticleEffect(this.particle, particlePosition, results, commandBuffer); + } + } + } + } } } } diff --git a/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/UsedTeleporter.java b/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/UsedTeleporter.java new file mode 100644 index 0000000..4720e93 --- /dev/null +++ b/src/com/hypixel/hytale/builtin/adventure/teleporter/interaction/server/UsedTeleporter.java @@ -0,0 +1,67 @@ +package com.hypixel.hytale.builtin.adventure.teleporter.interaction.server; + +import com.hypixel.hytale.builtin.adventure.teleporter.TeleporterPlugin; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import java.util.UUID; +import javax.annotation.Nullable; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +public class UsedTeleporter implements Component { + @Nullable + private UUID destinationWorldUuid; + private Vector3d destinationPosition; + private double clearOutXZ; + private double clearOutXZSquared; + private double clearOutY; + + public static ComponentType getComponentType() { + return TeleporterPlugin.get().getUsedTeleporterComponentType(); + } + + public UsedTeleporter() { + } + + public UsedTeleporter(@Nullable UUID destinationWorldUuid, Vector3d destinationPosition, double clearOutXZ, double clearOutY) { + this.destinationWorldUuid = destinationWorldUuid; + this.destinationPosition = destinationPosition; + this.clearOutXZ = clearOutXZ; + this.clearOutXZSquared = clearOutXZ * clearOutXZ; + this.clearOutY = clearOutY; + } + + @Nullable + public UUID getDestinationWorldUuid() { + return this.destinationWorldUuid; + } + + public Vector3d getDestinationPosition() { + return this.destinationPosition; + } + + public double getClearOutXZ() { + return this.clearOutXZ; + } + + public double getClearOutXZSquared() { + return this.clearOutXZSquared; + } + + public double getClearOutY() { + return this.clearOutY; + } + + @NullableDecl + @Override + public Component clone() { + UsedTeleporter clone = new UsedTeleporter(); + clone.destinationWorldUuid = this.destinationWorldUuid; + clone.destinationPosition = this.destinationPosition.clone(); + clone.clearOutXZ = this.clearOutXZ; + clone.clearOutXZSquared = this.clearOutXZSquared; + clone.clearOutY = this.clearOutY; + return clone; + } +} diff --git a/src/com/hypixel/hytale/builtin/adventure/teleporter/system/ClearUsedTeleporterSystem.java b/src/com/hypixel/hytale/builtin/adventure/teleporter/system/ClearUsedTeleporterSystem.java new file mode 100644 index 0000000..d514097 --- /dev/null +++ b/src/com/hypixel/hytale/builtin/adventure/teleporter/system/ClearUsedTeleporterSystem.java @@ -0,0 +1,60 @@ +package com.hypixel.hytale.builtin.adventure.teleporter.system; + +import com.hypixel.hytale.builtin.adventure.teleporter.interaction.server.UsedTeleporter; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.tick.EntityTickingSystem; +import com.hypixel.hytale.math.vector.Vector2d; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import java.util.UUID; +import javax.annotation.Nullable; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +public class ClearUsedTeleporterSystem extends EntityTickingSystem { + @Override + public void tick( + float dt, + int index, + @NonNullDecl ArchetypeChunk archetypeChunk, + @NonNullDecl Store store, + @NonNullDecl CommandBuffer commandBuffer + ) { + World world = store.getExternalData().getWorld(); + UsedTeleporter usedTeleporter = archetypeChunk.getComponent(index, UsedTeleporter.getComponentType()); + TransformComponent transformComponent = archetypeChunk.getComponent(index, TransformComponent.getComponentType()); + if (shouldClear(world, usedTeleporter, transformComponent)) { + Ref ref = archetypeChunk.getReferenceTo(index); + commandBuffer.removeComponent(ref, UsedTeleporter.getComponentType()); + } + } + + private static boolean shouldClear(World entityWorld, UsedTeleporter usedTeleporter, @Nullable TransformComponent transformComponent) { + if (transformComponent == null) { + return true; + } else { + UUID destinationWorldUuid = usedTeleporter.getDestinationWorldUuid(); + if (destinationWorldUuid != null && !entityWorld.getWorldConfig().getUuid().equals(destinationWorldUuid)) { + return true; + } else { + Vector3d entityPosition = transformComponent.getPosition(); + Vector3d destinationPosition = usedTeleporter.getDestinationPosition(); + double deltaY = Math.abs(entityPosition.y - destinationPosition.y); + double distanceXZsq = Vector2d.distanceSquared(entityPosition.x, entityPosition.z, destinationPosition.x, destinationPosition.z); + return deltaY > usedTeleporter.getClearOutY() || distanceXZsq > usedTeleporter.getClearOutXZ(); + } + } + } + + @NullableDecl + @Override + public Query getQuery() { + return UsedTeleporter.getComponentType(); + } +} diff --git a/src/com/hypixel/hytale/server/core/auth/AuthConfig.java b/src/com/hypixel/hytale/server/core/auth/AuthConfig.java index c87aae0..547938b 100644 --- a/src/com/hypixel/hytale/server/core/auth/AuthConfig.java +++ b/src/com/hypixel/hytale/server/core/auth/AuthConfig.java @@ -1,6 +1,7 @@ package com.hypixel.hytale.server.core.auth; import com.hypixel.hytale.common.util.java.ManifestUtil; +import java.time.Duration; import javax.annotation.Nonnull; public class AuthConfig { @@ -17,7 +18,7 @@ public class AuthConfig { public static final String SCOPE_CLIENT = "hytale:client"; public static final String SCOPE_SERVER = "hytale:server"; public static final String SCOPE_EDITOR = "hytale:editor"; - public static final int HTTP_TIMEOUT_SECONDS = 10; + public static final Duration HTTP_TIMEOUT = Duration.ofSeconds(30L); public static final int DEVICE_POLL_INTERVAL_SECONDS = 15; public static final String ENV_SERVER_AUDIENCE = "HYTALE_SERVER_AUDIENCE"; public static final String ENV_SERVER_IDENTITY_TOKEN = "HYTALE_SERVER_IDENTITY_TOKEN"; diff --git a/src/com/hypixel/hytale/server/core/auth/JWTValidator.java b/src/com/hypixel/hytale/server/core/auth/JWTValidator.java index b9f5f73..8398898 100644 --- a/src/com/hypixel/hytale/server/core/auth/JWTValidator.java +++ b/src/com/hypixel/hytale/server/core/auth/JWTValidator.java @@ -10,13 +10,13 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import java.security.cert.X509Certificate; import java.text.ParseException; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import javax.annotation.Nonnull; @@ -28,14 +28,14 @@ public class JWTValidator { private static final JWSAlgorithm SUPPORTED_ALGORITHM = JWSAlgorithm.EdDSA; private static final int MIN_SIGNATURE_LENGTH = 80; private static final int MAX_SIGNATURE_LENGTH = 90; + private static final Duration JWKS_REFRESH_MIN_INTERVAL = Duration.ofMinutes(5L); private final SessionServiceClient sessionServiceClient; private final String expectedIssuer; private final String expectedAudience; private volatile JWKSet cachedJwkSet; - private volatile long jwksCacheExpiry; - private final long jwksCacheDurationMs = TimeUnit.HOURS.toMillis(1L); private final ReentrantLock jwksFetchLock = new ReentrantLock(); private volatile CompletableFuture pendingFetch = null; + private volatile Instant lastJwksRefresh; public JWTValidator(@Nonnull SessionServiceClient sessionServiceClient, @Nonnull String expectedIssuer, @Nonnull String expectedAudience) { this.sessionServiceClient = sessionServiceClient; @@ -171,15 +171,14 @@ public class JWTValidator { @Nullable private JWKSet getJwkSet(boolean forceRefresh) { - long now = System.currentTimeMillis(); - if (!forceRefresh && this.cachedJwkSet != null && now < this.jwksCacheExpiry) { + if (!forceRefresh && this.cachedJwkSet != null) { return this.cachedJwkSet; } else { this.jwksFetchLock.lock(); - JWKSet var5; + JWKSet var3; try { - if (!forceRefresh && this.cachedJwkSet != null && now < this.jwksCacheExpiry) { + if (!forceRefresh && this.cachedJwkSet != null) { return this.cachedJwkSet; } @@ -196,7 +195,7 @@ public class JWTValidator { this.jwksFetchLock.unlock(); try { - var5 = existing.join(); + var3 = existing.join(); } finally { this.jwksFetchLock.lock(); } @@ -204,7 +203,7 @@ public class JWTValidator { this.jwksFetchLock.unlock(); } - return var5; + return var3; } } @@ -228,8 +227,8 @@ public class JWTValidator { } else { JWKSet newSet = new JWKSet(jwkList); this.cachedJwkSet = newSet; - this.jwksCacheExpiry = System.currentTimeMillis() + this.jwksCacheDurationMs; - LOGGER.at(Level.INFO).log("JWKS loaded with %d keys", jwkList.size()); + this.lastJwksRefresh = Instant.now(); + LOGGER.at(Level.INFO).log("JWKS loaded with %d keys (cached permanently)", jwkList.size()); return newSet; } } catch (Exception var8) { @@ -248,6 +247,9 @@ public class JWTValidator { return false; } else if (this.verifySignature(signedJWT, jwkSet)) { return true; + } else if (!this.canForceRefreshJwks()) { + LOGGER.at(Level.FINE).log("Signature verification failed but JWKS was refreshed recently; skipping refresh"); + return false; } else { LOGGER.at(Level.INFO).log("Signature verification failed with cached JWKS, retrying with fresh keys"); JWKSet freshJwkSet = this.getJwkSet(true); @@ -255,6 +257,11 @@ public class JWTValidator { } } + private boolean canForceRefreshJwks() { + Instant lastRefresh = this.lastJwksRefresh; + return lastRefresh == null ? true : Duration.between(lastRefresh, Instant.now()).compareTo(JWKS_REFRESH_MIN_INTERVAL) >= 0; + } + @Nullable private JWK convertToJWK(SessionServiceClient.JwkKey key) { if (!"OKP".equals(key.kty)) { @@ -276,7 +283,6 @@ public class JWTValidator { try { this.cachedJwkSet = null; - this.jwksCacheExpiry = 0L; this.pendingFetch = null; } finally { this.jwksFetchLock.unlock(); diff --git a/src/com/hypixel/hytale/server/core/auth/ProfileServiceClient.java b/src/com/hypixel/hytale/server/core/auth/ProfileServiceClient.java index 2c9dc26..c903f90 100644 --- a/src/com/hypixel/hytale/server/core/auth/ProfileServiceClient.java +++ b/src/com/hypixel/hytale/server/core/auth/ProfileServiceClient.java @@ -6,6 +6,7 @@ import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.codec.util.RawJsonReader; import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.util.ServiceHttpClientFactory; import java.io.IOException; import java.net.URI; import java.net.URLEncoder; @@ -14,7 +15,6 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -23,14 +23,13 @@ import javax.annotation.Nullable; public class ProfileServiceClient { private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); - private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(5L); private final HttpClient httpClient; private final String profileServiceUrl; public ProfileServiceClient(@Nonnull String profileServiceUrl) { if (profileServiceUrl != null && !profileServiceUrl.isEmpty()) { this.profileServiceUrl = profileServiceUrl.endsWith("/") ? profileServiceUrl.substring(0, profileServiceUrl.length() - 1) : profileServiceUrl; - this.httpClient = HttpClient.newBuilder().connectTimeout(REQUEST_TIMEOUT).build(); + this.httpClient = ServiceHttpClientFactory.create(AuthConfig.HTTP_TIMEOUT); LOGGER.at(Level.INFO).log("Profile Service client initialized for: %s", this.profileServiceUrl); } else { throw new IllegalArgumentException("Profile Service URL cannot be null or empty"); @@ -45,7 +44,7 @@ public class ProfileServiceClient { .header("Accept", "application/json") .header("Authorization", "Bearer " + bearerToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .GET() .build(); LOGGER.at(Level.FINE).log("Fetching profile by UUID: %s", uuid); @@ -90,7 +89,7 @@ public class ProfileServiceClient { .header("Accept", "application/json") .header("Authorization", "Bearer " + bearerToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .GET() .build(); LOGGER.at(Level.FINE).log("Fetching profile by username: %s", username); diff --git a/src/com/hypixel/hytale/server/core/auth/SessionServiceClient.java b/src/com/hypixel/hytale/server/core/auth/SessionServiceClient.java index d6eb0c4..b79298f 100644 --- a/src/com/hypixel/hytale/server/core/auth/SessionServiceClient.java +++ b/src/com/hypixel/hytale/server/core/auth/SessionServiceClient.java @@ -7,6 +7,7 @@ import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.codec.codecs.array.ArrayCodec; import com.hypixel.hytale.codec.util.RawJsonReader; import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.util.ServiceHttpClientFactory; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -14,24 +15,25 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; -import java.time.Duration; import java.time.Instant; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class SessionServiceClient { private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); - private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(5L); + private static final ExecutorService HTTP_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); private final HttpClient httpClient; private final String sessionServiceUrl; public SessionServiceClient(@Nonnull String sessionServiceUrl) { if (sessionServiceUrl != null && !sessionServiceUrl.isEmpty()) { this.sessionServiceUrl = sessionServiceUrl.endsWith("/") ? sessionServiceUrl.substring(0, sessionServiceUrl.length() - 1) : sessionServiceUrl; - this.httpClient = HttpClient.newBuilder().connectTimeout(REQUEST_TIMEOUT).build(); + this.httpClient = ServiceHttpClientFactory.create(AuthConfig.HTTP_TIMEOUT); LOGGER.at(Level.INFO).log("Session Service client initialized for: %s", this.sessionServiceUrl); } else { throw new IllegalArgumentException("Session Service URL cannot be null or empty"); @@ -49,7 +51,7 @@ public class SessionServiceClient { .header("Accept", "application/json") .header("Authorization", "Bearer " + bearerToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .POST(BodyPublishers.ofString(jsonBody)) .build(); LOGGER.at(Level.INFO).log("Requesting authorization grant with identity token, aud='%s'", serverAudience); @@ -79,7 +81,8 @@ public class SessionServiceClient { LOGGER.at(Level.WARNING).log("Unexpected error requesting authorization grant: %s", var10.getMessage()); return null; } - } + }, + HTTP_EXECUTOR ); } @@ -98,7 +101,7 @@ public class SessionServiceClient { .header("Accept", "application/json") .header("Authorization", "Bearer " + bearerToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .POST(BodyPublishers.ofString(jsonBody)) .build(); LOGGER.at(Level.INFO).log("Exchanging authorization grant for access token"); @@ -130,7 +133,8 @@ public class SessionServiceClient { LOGGER.at(Level.WARNING).log("Unexpected error exchanging auth grant: %s", var10.getMessage()); return null; } - } + }, + HTTP_EXECUTOR ); } @@ -141,7 +145,7 @@ public class SessionServiceClient { .uri(URI.create(this.sessionServiceUrl + "/.well-known/jwks.json")) .header("Accept", "application/json") .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .GET() .build(); LOGGER.at(Level.FINE).log("Fetching JWKS from Session Service"); @@ -181,7 +185,7 @@ public class SessionServiceClient { .header("Accept", "application/json") .header("Authorization", "Bearer " + oauthAccessToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .GET() .build(); LOGGER.at(Level.INFO).log("Fetching game profiles..."); @@ -221,7 +225,7 @@ public class SessionServiceClient { .header("Content-Type", "application/json") .header("Authorization", "Bearer " + oauthAccessToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .POST(BodyPublishers.ofString(body)) .build(); LOGGER.at(Level.INFO).log("Creating game session..."); @@ -262,7 +266,7 @@ public class SessionServiceClient { .header("Accept", "application/json") .header("Authorization", "Bearer " + sessionToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .POST(BodyPublishers.noBody()) .build(); LOGGER.at(Level.INFO).log("Refreshing game session..."); @@ -292,7 +296,8 @@ public class SessionServiceClient { LOGGER.at(Level.WARNING).log("Unexpected error refreshing session: %s", var7.getMessage()); return null; } - } + }, + HTTP_EXECUTOR ); } @@ -303,7 +308,7 @@ public class SessionServiceClient { .uri(URI.create(this.sessionServiceUrl + "/game-session")) .header("Authorization", "Bearer " + sessionToken) .header("User-Agent", AuthConfig.USER_AGENT) - .timeout(REQUEST_TIMEOUT) + .timeout(AuthConfig.HTTP_TIMEOUT) .DELETE() .build(); LOGGER.at(Level.INFO).log("Terminating game session..."); diff --git a/src/com/hypixel/hytale/server/core/auth/oauth/OAuthClient.java b/src/com/hypixel/hytale/server/core/auth/oauth/OAuthClient.java index 097b609..58b144c 100644 --- a/src/com/hypixel/hytale/server/core/auth/oauth/OAuthClient.java +++ b/src/com/hypixel/hytale/server/core/auth/oauth/OAuthClient.java @@ -6,6 +6,7 @@ import com.google.gson.JsonParser; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.server.core.HytaleServer; import com.hypixel.hytale.server.core.auth.AuthConfig; +import com.hypixel.hytale.server.core.util.ServiceHttpClientFactory; import com.sun.net.httpserver.HttpServer; import java.io.OutputStream; import java.net.InetSocketAddress; @@ -20,7 +21,6 @@ import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; -import java.time.Duration; import java.util.Base64; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -34,7 +34,7 @@ import javax.annotation.Nullable; public class OAuthClient { private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); private static final SecureRandom RANDOM = new SecureRandom(); - private final HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10L)).build(); + private final HttpClient httpClient = ServiceHttpClientFactory.create(AuthConfig.HTTP_TIMEOUT); public Runnable startFlow(@Nonnull OAuthBrowserFlow flow) { AtomicBoolean cancelled = new AtomicBoolean(false); diff --git a/src/com/hypixel/hytale/server/core/entity/InteractionManager.java b/src/com/hypixel/hytale/server/core/entity/InteractionManager.java index 5974f02..457be61 100644 --- a/src/com/hypixel/hytale/server/core/entity/InteractionManager.java +++ b/src/com/hypixel/hytale/server/core/entity/InteractionManager.java @@ -10,7 +10,6 @@ import com.hypixel.hytale.function.function.TriFunction; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.math.util.ChunkUtil; import com.hypixel.hytale.math.vector.Vector4d; -import com.hypixel.hytale.metrics.metric.HistoricMetric; import com.hypixel.hytale.protocol.BlockPosition; import com.hypixel.hytale.protocol.ForkedChainId; import com.hypixel.hytale.protocol.GameMode; @@ -44,11 +43,6 @@ import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.util.UUIDUtil; -import io.sentry.Sentry; -import io.sentry.SentryEvent; -import io.sentry.SentryLevel; -import io.sentry.protocol.Message; -import io.sentry.protocol.SentryId; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -56,7 +50,6 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectList; import java.util.Arrays; import java.util.Deque; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -197,7 +190,7 @@ public class InteractionManager implements Component { int highestChainId = -1; boolean changed = false; - label99: + label114: while (it.hasNext()) { SyncInteractionChain packet = it.next(); if (packet.desync) { @@ -233,7 +226,7 @@ public class InteractionManager implements Component { it.remove(); this.packetQueueTime = 0L; } - continue label99; + continue label114; } chain = subChain; @@ -241,50 +234,51 @@ public class InteractionManager implements Component { } highestChainId = Math.max(highestChainId, packet.chainId); - if (chain == null && !finished) { - if (this.syncStart(ref, packet)) { + boolean isProxy = packet.data != null && !UUIDUtil.isEmptyOrNull(packet.data.proxyId); + if ((chain != null || finished) && !isProxy) { + if (chain != null) { + this.sync(ref, chain, packet); changed = true; it.remove(); this.packetQueueTime = 0L; - } else { - if (!this.waitingForClient(ref)) { - long queuedTime; - if (this.packetQueueTime == 0L) { - this.packetQueueTime = this.currentTime; - queuedTime = 0L; - } else { - queuedTime = this.currentTime - this.packetQueueTime; - } - - HytaleLogger.Api context = LOGGER.at(Level.FINE); - if (context.isEnabled()) { - context.log("Queued chain %d for %s", packet.chainId, FormatUtil.nanosToString(queuedTime)); - } - - if (queuedTime > TimeUnit.MILLISECONDS.toNanos(this.getOperationTimeoutThreshold())) { - this.sendCancelPacket(packet.chainId, packet.forkedId); - it.remove(); - context = LOGGER.at(Level.FINE); - if (context.isEnabled()) { - context.log("Discarding packet due to queuing for too long: %s", packet); - } - } - } - - if (!desynced) { - finished = true; - } + } else if (desynced) { + this.sendCancelPacket(packet.chainId, packet.forkedId); + it.remove(); + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + ctx.log("Discarding packet due to desync: %s", packet); } - } else if (chain != null) { - this.sync(ref, chain, packet); + } else if (this.syncStart(ref, packet)) { changed = true; it.remove(); this.packetQueueTime = 0L; - } else if (desynced) { - this.sendCancelPacket(packet.chainId, packet.forkedId); - it.remove(); - HytaleLogger.Api ctx = LOGGER.at(Level.FINE); - ctx.log("Discarding packet due to desync: %s", packet); + } else { + if (!this.waitingForClient(ref)) { + long queuedTime; + if (this.packetQueueTime == 0L) { + this.packetQueueTime = this.currentTime; + queuedTime = 0L; + } else { + queuedTime = this.currentTime - this.packetQueueTime; + } + + HytaleLogger.Api context = LOGGER.at(Level.FINE); + if (context.isEnabled()) { + context.log("Queued chain %d for %s", packet.chainId, FormatUtil.nanosToString(queuedTime)); + } + + if (queuedTime > TimeUnit.MILLISECONDS.toNanos(this.getOperationTimeoutThreshold())) { + this.sendCancelPacket(packet.chainId, packet.forkedId); + it.remove(); + context = LOGGER.at(Level.FINE); + if (context.isEnabled()) { + context.log("Discarding packet due to queuing for too long: %s", packet); + } + } + } + + if (!desynced && !isProxy) { + finished = true; + } } } @@ -363,7 +357,7 @@ public class InteractionManager implements Component { long threshold = this.getOperationTimeoutThreshold(); TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType()); if (timeResource.getTimeDilationModifier() == 1.0F && waitMillis > threshold) { - this.sendCancelPacket(chain); + this.cancelChains(chain); return chain.getForkedChains().isEmpty(); } } @@ -641,41 +635,10 @@ public class InteractionManager implements Component { long threshold = this.getOperationTimeoutThreshold(); if (tickTimeDilation == 1.0F && waitMillis > threshold) { - SentryEvent event = new SentryEvent(); - event.setLevel(SentryLevel.ERROR); - Message message = new Message(); - message.setMessage("Client failed to send client data, ending early to prevent desync"); - HashMap unknown = new HashMap<>(); - unknown.put("Threshold", threshold); - unknown.put("Wait Millis", waitMillis); - unknown.put("Current Root", chain.getRootInteraction() != null ? chain.getRootInteraction().getId() : ""); - Operation innerOp = operation.getInnerOperation(); - unknown.put("Current Op", innerOp.getClass().getName()); - if (innerOp instanceof Interaction interaction) { - unknown.put("Current Interaction", interaction.getId()); - } - - unknown.put("Current Index", chain.getOperationIndex()); - unknown.put("Current Op Counter", chain.getOperationCounter()); - HistoricMetric metric = ref.getStore().getExternalData().getWorld().getBufferedTickLengthMetricSet(); - long[] periods = metric.getPeriodsNanos(); - - for (int i = 0; i < periods.length; i++) { - String length = FormatUtil.timeUnitToString(periods[i], TimeUnit.NANOSECONDS, true); - double average = metric.getAverage(i); - long min = metric.calculateMin(i); - long max = metric.calculateMax(i); - String value = FormatUtil.simpleTimeUnitFormat(min, average, max, TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS, 3); - unknown.put(String.format("World Perf %s", length), value); - } - - event.setExtras(unknown); - event.setMessage(message); - SentryId eventId = Sentry.captureEvent(event); - LOGGER.atWarning().log("Client failed to send client data, ending early to prevent desync. %s", eventId); + LOGGER.atWarning().log("Client failed to send client data, ending early to prevent desync."); chain.setServerState(InteractionState.Failed); chain.setClientState(InteractionState.Failed); - this.sendCancelPacket(chain); + this.cancelChains(chain); return null; } else { if (entry.consumeSendInitial() || wasWrong) { @@ -768,6 +731,8 @@ public class InteractionManager implements Component { if (ctx.isEnabled()) { ctx.log("Got syncStart for %d-%s but packet wasn't the first.", index, packet.forkedId); } + + this.sendCancelPacket(index, packet.forkedId); } return true; @@ -777,6 +742,7 @@ public class InteractionManager implements Component { ctx.log("Can't start a forked chain from the client: %d %s", index, packet.forkedId); } + this.sendCancelPacket(index, packet.forkedId); return true; } else { InteractionType type = packet.interactionType; @@ -1362,7 +1328,7 @@ public class InteractionManager implements Component { this.sendCancelPacket(chain.getChainId(), chain.getForkedChainId()); } - public void sendCancelPacket(int chainId, @Nonnull ForkedChainId forkedChainId) { + public void sendCancelPacket(int chainId, ForkedChainId forkedChainId) { if (this.playerRef != null) { this.playerRef.getPacketHandler().writeNoCache(new CancelInteractionChain(chainId, forkedChainId)); } diff --git a/src/com/hypixel/hytale/server/core/inventory/Inventory.java b/src/com/hypixel/hytale/server/core/inventory/Inventory.java index a6e8529..2b7c5f9 100644 --- a/src/com/hypixel/hytale/server/core/inventory/Inventory.java +++ b/src/com/hypixel/hytale/server/core/inventory/Inventory.java @@ -456,13 +456,17 @@ public class Inventory implements NetworkSerializable { private boolean tryEquipArmorPart(int fromSectionId, short fromSlotId, int quantity, ItemContainer targetContainer, boolean forceEquip) { ItemStack itemStack = targetContainer.getItemStack(fromSlotId); - Item item = itemStack.getItem(); - ItemArmor itemArmor = item.getArmor(); - if (itemArmor == null || fromSectionId == -3 || !forceEquip && this.armor.getItemStack((short)itemArmor.getArmorSlot().ordinal()) != null) { + if (ItemStack.isEmpty(itemStack)) { return false; } else { - targetContainer.moveItemStackFromSlotToSlot(fromSlotId, quantity, this.armor, (short)itemArmor.getArmorSlot().ordinal()); - return true; + Item item = itemStack.getItem(); + ItemArmor itemArmor = item.getArmor(); + if (itemArmor == null || fromSectionId == -3 || !forceEquip && this.armor.getItemStack((short)itemArmor.getArmorSlot().ordinal()) != null) { + return false; + } else { + targetContainer.moveItemStackFromSlotToSlot(fromSlotId, quantity, this.armor, (short)itemArmor.getArmorSlot().ordinal()); + return true; + } } } diff --git a/src/com/hypixel/hytale/server/core/io/PacketHandler.java b/src/com/hypixel/hytale/server/core/io/PacketHandler.java index 69da3f8..102e5cc 100644 --- a/src/com/hypixel/hytale/server/core/io/PacketHandler.java +++ b/src/com/hypixel/hytale/server/core/io/PacketHandler.java @@ -410,10 +410,12 @@ public abstract class PacketHandler implements IPacketReceiver { Attribute loginStartAttribute = channel.attr(LOGIN_START_ATTRIBUTE_KEY); long now = System.nanoTime(); Long before = loginStartAttribute.getAndSet(now); + NettyUtil.TimeoutContext context = channel.attr(NettyUtil.TimeoutContext.KEY).get(); + String identifier = context != null ? context.playerIdentifier() : NettyUtil.formatRemoteAddress(channel); if (before == null) { - LOGIN_TIMING_LOGGER.at(level).log(message); + LOGIN_TIMING_LOGGER.at(level).log("[%s] %s", identifier, message); } else { - LOGIN_TIMING_LOGGER.at(level).log("%s took %s", message, LazyArgs.lazy(() -> FormatUtil.nanosToString(now - before))); + LOGIN_TIMING_LOGGER.at(level).log("[%s] %s took %s", identifier, message, LazyArgs.lazy(() -> FormatUtil.nanosToString(now - before))); } } diff --git a/src/com/hypixel/hytale/server/core/universe/Universe.java b/src/com/hypixel/hytale/server/core/universe/Universe.java index 5b584fd..baef1b1 100644 --- a/src/com/hypixel/hytale/server/core/universe/Universe.java +++ b/src/com/hypixel/hytale/server/core/universe/Universe.java @@ -688,42 +688,60 @@ public class Universe extends JavaPlugin implements IMessageReceiver, MetricProv .getEventBus() .dispatchFor(PlayerConnectEvent.class) .dispatch(new PlayerConnectEvent((Holder)holder, playerRefComponent, lastWorld != null ? lastWorld : this.getDefaultWorld())); - World world = event.getWorld() != null ? event.getWorld() : this.getDefaultWorld(); - if (world == null) { + if (!channel.isActive()) { this.players.remove(uuid, playerRefComponent); - playerConnection.disconnect("No world available to join"); - this.getLogger().at(Level.SEVERE).log("Player '%s' (%s) could not join - no default world configured", username, uuid); + this.getLogger().at(Level.INFO).log("Player '%s' (%s) disconnected during PlayerConnectEvent, cleaned up", username, uuid); return CompletableFuture.completedFuture(null); } else { - if (lastWorldName != null && lastWorld == null) { - playerComponent.sendMessage( - Message.translation("server.universe.failedToFindWorld").param("lastWorldName", lastWorldName).param("name", world.getName()) - ); - } - - PacketHandler.logConnectionTimings(channel, "Processed Referral", Level.FINEST); - playerRefComponent.getPacketHandler().write(new ServerTags(AssetRegistry.getClientTags())); - return world.addPlayer(playerRefComponent, null, false, false).thenApply(p -> { - PacketHandler.logConnectionTimings(channel, "Add to World", Level.FINEST); - if (!channel.isActive()) { - if (p != null) { - playerComponent.remove(); - } - - this.players.remove(uuid, playerRefComponent); - this.getLogger().at(Level.WARNING).log("Player '%s' (%s) disconnected during world join, cleaned up from universe", username, uuid); - return null; - } else if (playerComponent.wasRemoved()) { - this.players.remove(uuid, playerRefComponent); - return null; - } else { - return (PlayerRef)p; - } - }).exceptionally(throwable -> { + World world = event.getWorld() != null ? event.getWorld() : this.getDefaultWorld(); + if (world == null) { this.players.remove(uuid, playerRefComponent); - playerComponent.remove(); - throw new RuntimeException("Exception when adding player to universe:", throwable); - }); + playerConnection.disconnect("No world available to join"); + this.getLogger().at(Level.SEVERE).log("Player '%s' (%s) could not join - no default world configured", username, uuid); + return CompletableFuture.completedFuture(null); + } else { + if (lastWorldName != null && lastWorld == null) { + playerComponent.sendMessage( + Message.translation("server.universe.failedToFindWorld").param("lastWorldName", lastWorldName).param("name", world.getName()) + ); + } + + PacketHandler.logConnectionTimings(channel, "Processed Referral", Level.FINEST); + playerRefComponent.getPacketHandler().write(new ServerTags(AssetRegistry.getClientTags())); + CompletableFuture addPlayerFuture = world.addPlayer(playerRefComponent, null, false, false); + if (addPlayerFuture == null) { + this.players.remove(uuid, playerRefComponent); + this.getLogger().at(Level.INFO).log("Player '%s' (%s) disconnected before world addition, cleaned up", username, uuid); + return CompletableFuture.completedFuture(null); + } else { + return addPlayerFuture.thenApply( + p -> { + PacketHandler.logConnectionTimings(channel, "Add to World", Level.FINEST); + if (!channel.isActive()) { + if (p != null) { + playerComponent.remove(); + } + + this.players.remove(uuid, playerRefComponent); + this.getLogger() + .at(Level.WARNING) + .log("Player '%s' (%s) disconnected during world join, cleaned up from universe", username, uuid); + return null; + } else if (playerComponent.wasRemoved()) { + this.players.remove(uuid, playerRefComponent); + return null; + } else { + return (PlayerRef)p; + } + } + ) + .exceptionally(throwable -> { + this.players.remove(uuid, playerRefComponent); + playerComponent.remove(); + throw new RuntimeException("Exception when adding player to universe:", throwable); + }); + } + } } } } diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java b/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java index 7c4ae4e..c8bd04f 100644 --- a/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java @@ -58,7 +58,7 @@ public class IndexedStorageChunkStorageProvider implements IChunkStorageProvider ) .add() .build(); - private boolean flushOnWrite = true; + private boolean flushOnWrite = false; @Nonnull @Override diff --git a/src/com/hypixel/hytale/server/core/update/UpdateService.java b/src/com/hypixel/hytale/server/core/update/UpdateService.java index fd66b04..2214192 100644 --- a/src/com/hypixel/hytale/server/core/update/UpdateService.java +++ b/src/com/hypixel/hytale/server/core/update/UpdateService.java @@ -11,6 +11,7 @@ import com.hypixel.hytale.server.core.HytaleServer; import com.hypixel.hytale.server.core.HytaleServerConfig; import com.hypixel.hytale.server.core.auth.AuthConfig; import com.hypixel.hytale.server.core.auth.ServerAuthManager; +import com.hypixel.hytale.server.core.util.ServiceHttpClientFactory; import com.hypixel.hytale.server.core.util.io.FileUtil; import java.io.IOException; import java.io.InputStream; @@ -46,7 +47,7 @@ public class UpdateService { private final String accountDataUrl = "https://account-data.hytale.com"; public UpdateService() { - this.httpClient = HttpClient.newBuilder().connectTimeout(REQUEST_TIMEOUT).followRedirects(Redirect.NORMAL).build(); + this.httpClient = ServiceHttpClientFactory.newBuilder(REQUEST_TIMEOUT).followRedirects(Redirect.NORMAL).build(); } @Nullable diff --git a/src/com/hypixel/hytale/server/core/util/ServiceHttpClientFactory.java b/src/com/hypixel/hytale/server/core/util/ServiceHttpClientFactory.java new file mode 100644 index 0000000..212c478 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/util/ServiceHttpClientFactory.java @@ -0,0 +1,23 @@ +package com.hypixel.hytale.server.core.util; + +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.time.Duration; +import java.util.Objects; +import javax.annotation.Nonnull; + +public final class ServiceHttpClientFactory { + private ServiceHttpClientFactory() { + } + + @Nonnull + public static Builder newBuilder(@Nonnull Duration connectTimeout) { + Objects.requireNonNull(connectTimeout, "connectTimeout"); + return HttpClient.newBuilder().connectTimeout(connectTimeout); + } + + @Nonnull + public static HttpClient create(@Nonnull Duration connectTimeout) { + return newBuilder(connectTimeout).build(); + } +}