Compare commits

...

3 Commits

Author SHA1 Message Date
Intege-rs
480cf742c8 2026.01.28-87d03be09 2026-01-28 08:30:21 -05:00
Intege-rs
66b236b50e 2026.01.27-734d39026 2026-01-27 21:50:02 -05:00
Intege-rs
3fbbfeb54b 2026.01.24-6e2d4fc36 2026-01-27 21:48:16 -05:00
18 changed files with 406 additions and 201 deletions

7
.gitignore vendored
View File

@@ -1,5 +1,4 @@
build/
bin/
.kotlin
.idea
@@ -15,6 +14,12 @@ out/
!**/src/main/**/out/
!**/src/test/**/out/
### MCP adds eclipser
bin/
.classpath
.project
.eclipse
# Gradle Wrapper
gradlew
gradlew.bat

View File

@@ -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<ChunkStore, Teleporter> teleporterComponentType;
private ComponentType<EntityStore, UsedTeleporter> 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<EntityStore, UsedTeleporter> getUsedTeleporterComponentType() {
return this.usedTeleporterComponentType;
}
private static class TeleporterOwnedWarpRefChangeSystem extends RefChangeSystem<ChunkStore, Teleporter> {
@Nonnull
@Override

View File

@@ -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()
.<Double>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()
.<Double>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<EntityStore> 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<Ref<EntityStore>, EntityStore> playerSpatialResource = commandBuffer.getResource(
EntityModule.get().getPlayerSpatialResourceType()
);
ObjectList<Ref<EntityStore>> 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<Ref<EntityStore>, EntityStore> playerSpatialResource = commandBuffer.getResource(
EntityModule.get().getPlayerSpatialResourceType()
);
ObjectList<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
playerSpatialResource.getSpatialStructure().collect(particlePosition, 75.0, results);
ParticleUtil.spawnParticleEffect(this.particle, particlePosition, results, commandBuffer);
}
}
}
}
}
}
}

View File

@@ -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<EntityStore> {
@Nullable
private UUID destinationWorldUuid;
private Vector3d destinationPosition;
private double clearOutXZ;
private double clearOutXZSquared;
private double clearOutY;
public static ComponentType<EntityStore, UsedTeleporter> 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<EntityStore> 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;
}
}

View File

@@ -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<EntityStore> {
@Override
public void tick(
float dt,
int index,
@NonNullDecl ArchetypeChunk<EntityStore> archetypeChunk,
@NonNullDecl Store<EntityStore> store,
@NonNullDecl CommandBuffer<EntityStore> 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<EntityStore> 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<EntityStore> getQuery() {
return UsedTeleporter.getComponentType();
}
}

View File

@@ -65,22 +65,28 @@ public class AssetEditorGamePacketHandler implements SubPacketHandler {
if (ref != null && ref.isValid()) {
Store<EntityStore> store = ref.getStore();
World world = store.getExternalData().getWorld();
CompletableFuture.runAsync(() -> {
world.execute(
() -> {
Player playerComponent = store.getComponent(ref, Player.getComponentType());
if (!this.lacksPermission(playerComponent, true)) {
;
CompletableFuture.runAsync(
() -> {
LOGGER.at(Level.INFO).log("%s updating json asset at %s", this.packetHandler.getPlayerRef().getUsername(), packet.path);
EditorClient mockClient = new EditorClient(playerRef);
AssetEditorPlugin.get()
.handleJsonAssetUpdate(
mockClient,
packet.path != null ? new AssetPath(packet.path) : null,
packet.assetType,
packet.assetIndex,
packet.commands,
packet.token
);
}
);
}
}, world)
.thenRunAsync(
() -> {
LOGGER.at(Level.INFO).log("%s updating json asset at %s", this.packetHandler.getPlayerRef().getUsername(), packet.path);
EditorClient mockClient = new EditorClient(playerRef);
AssetEditorPlugin.get()
.handleJsonAssetUpdate(
mockClient, packet.path != null ? new AssetPath(packet.path) : null, packet.assetType, packet.assetIndex, packet.commands, packet.token
);
}
);
}
);
} else {
throw new RuntimeException("Unable to process AssetEditorUpdateJsonAsset packet. Player ref is invalid!");
}

View File

@@ -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";

View File

@@ -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<JWKSet> 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();

View File

@@ -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);

View File

@@ -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...");

View File

@@ -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);

View File

@@ -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<EntityStore> {
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<EntityStore> {
it.remove();
this.packetQueueTime = 0L;
}
continue label99;
continue label114;
}
chain = subChain;
@@ -241,50 +234,51 @@ public class InteractionManager implements Component<EntityStore> {
}
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<EntityStore> {
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<EntityStore> {
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<String, Object> unknown = new HashMap<>();
unknown.put("Threshold", threshold);
unknown.put("Wait Millis", waitMillis);
unknown.put("Current Root", chain.getRootInteraction() != null ? chain.getRootInteraction().getId() : "<null>");
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<EntityStore> {
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<EntityStore> {
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<EntityStore> {
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));
}

View File

@@ -456,13 +456,17 @@ public class Inventory implements NetworkSerializable<UpdatePlayerInventory> {
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;
}
}
}

View File

@@ -410,10 +410,12 @@ public abstract class PacketHandler implements IPacketReceiver {
Attribute<Long> 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)));
}
}

View File

@@ -688,42 +688,60 @@ public class Universe extends JavaPlugin implements IMessageReceiver, MetricProv
.getEventBus()
.<Void, PlayerConnectEvent>dispatchFor(PlayerConnectEvent.class)
.dispatch(new PlayerConnectEvent((Holder<EntityStore>)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<PlayerRef> 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.<PlayerRef>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);
});
}
}
}
}
}

View File

@@ -58,7 +58,7 @@ public class IndexedStorageChunkStorageProvider implements IChunkStorageProvider
)
.add()
.build();
private boolean flushOnWrite = true;
private boolean flushOnWrite = false;
@Nonnull
@Override

View File

@@ -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

View File

@@ -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();
}
}