hytale-server/com/hypixel/hytale/builtin/portals/ui/PortalDeviceSummonPage.java

463 lines
26 KiB
Java

package com.hypixel.hytale.builtin.portals.ui;
import com.hypixel.hytale.builtin.instances.InstancesPlugin;
import com.hypixel.hytale.builtin.instances.config.InstanceDiscoveryConfig;
import com.hypixel.hytale.builtin.instances.config.InstanceWorldConfig;
import com.hypixel.hytale.builtin.portals.PortalsPlugin;
import com.hypixel.hytale.builtin.portals.components.PortalDevice;
import com.hypixel.hytale.builtin.portals.components.PortalDeviceConfig;
import com.hypixel.hytale.builtin.portals.integrations.PortalGameplayConfig;
import com.hypixel.hytale.builtin.portals.integrations.PortalRemovalCondition;
import com.hypixel.hytale.builtin.portals.resources.PortalWorld;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.math.vector.Transform;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.protocol.SoundCategory;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.protocol.packets.interface_.Page;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.asset.type.gameplay.GameplayConfig;
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
import com.hypixel.hytale.server.core.asset.type.item.config.PortalKey;
import com.hypixel.hytale.server.core.asset.type.portalworld.PillTag;
import com.hypixel.hytale.server.core.asset.type.portalworld.PortalDescription;
import com.hypixel.hytale.server.core.asset.type.portalworld.PortalSpawn;
import com.hypixel.hytale.server.core.asset.type.portalworld.PortalType;
import com.hypixel.hytale.server.core.asset.util.ColorParseUtil;
import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.inventory.Inventory;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.modules.block.BlockModule;
import com.hypixel.hytale.server.core.ui.builder.EventData;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
import com.hypixel.hytale.server.core.universe.world.spawn.IndividualSpawnProvider;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class PortalDeviceSummonPage extends InteractiveCustomUIPage<PortalDeviceSummonPage.Data> {
private final PortalDeviceConfig config;
private final Ref<ChunkStore> blockRef;
private final ItemStack offeredItemStack;
private static final Transform DEFAULT_WORLDGEN_SPAWN = new Transform(0.0, 140.0, 0.0);
public PortalDeviceSummonPage(@Nonnull PlayerRef playerRef, PortalDeviceConfig config, Ref<ChunkStore> blockRef, ItemStack offeredItemStack) {
super(playerRef, CustomPageLifetime.CanDismissOrCloseThroughInteraction, PortalDeviceSummonPage.Data.CODEC);
this.config = config;
this.blockRef = blockRef;
this.offeredItemStack = offeredItemStack;
}
@Override
public void build(
@Nonnull Ref<EntityStore> ref, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder, @Nonnull Store<EntityStore> store
) {
Player playerComponent = store.getComponent(ref, Player.getComponentType());
assert playerComponent != null;
PortalDeviceSummonPage.State state = this.computeState(playerComponent, store);
if (state != PortalDeviceSummonPage.Error.INVALID_BLOCK) {
if (state instanceof PortalDeviceSummonPage.CanSpawnPortal canSpawn) {
commandBuilder.append("Pages/PortalDeviceSummon.ui");
PortalKey var22 = canSpawn.portalKey();
PortalType portalType = canSpawn.portalType();
PortalDescription var23 = portalType.getDescription();
commandBuilder.set("#Artwork.Background", "Pages/Portals/" + var23.getSplashImageFilename());
commandBuilder.set("#Title0.TextSpans", var23.getDisplayName());
commandBuilder.set("#FlavorLabel.TextSpans", var23.getFlavorText());
updateCustomPills(commandBuilder, portalType);
String[] objectivesKeys = var23.getObjectivesKeys();
String[] var25 = var23.getWisdomKeys();
commandBuilder.set("#Objectives.Visible", objectivesKeys.length > 0);
commandBuilder.set("#Tips.Visible", var25.length > 0);
updateBulletList(commandBuilder, "#ObjectivesList", objectivesKeys);
updateBulletList(commandBuilder, "#TipsList", var25);
PortalGameplayConfig gameplayConfig = portalType.getGameplayConfig().getPluginConfig().get(PortalGameplayConfig.class);
long totalTimeLimit = TimeUnit.SECONDS.toMinutes(var22.getTimeLimitSeconds());
if (portalType.isVoidInvasionEnabled()) {
long minutesBreach = TimeUnit.SECONDS.toMinutes(gameplayConfig.getVoidEvent().getDurationSeconds());
long exploMinutes = totalTimeLimit - minutesBreach;
commandBuilder.set(
"#ExplorationTimeText.TextSpans", Message.translation("server.customUI.portalDevice.minutesToExplore").param("time", exploMinutes)
);
commandBuilder.set("#BreachTimeBullet.Visible", true);
commandBuilder.set(
"#BreachTimeText.TextSpans", Message.translation("server.customUI.portalDevice.minutesVoidInvasion").param("time", minutesBreach)
);
} else {
commandBuilder.set(
"#ExplorationTimeText.TextSpans", Message.translation("server.customUI.portalDevice.durationMins").param("time", totalTimeLimit)
);
}
eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#SummonButton", EventData.of("Action", "SummonActivated"), false);
eventBuilder.addEventBinding(CustomUIEventBindingType.MouseEntered, "#SummonButton", EventData.of("Action", "SummonMouseEntered"), false);
eventBuilder.addEventBinding(CustomUIEventBindingType.MouseExited, "#SummonButton", EventData.of("Action", "SummonMouseExited"), false);
} else {
commandBuilder.append("Pages/PortalDeviceError.ui");
if (state == PortalDeviceSummonPage.Error.NOTHING_OFFERED || state == PortalDeviceSummonPage.Error.NOT_A_PORTAL_KEY) {
commandBuilder.set("#UsageErrorTitle.Text", Message.translation("server.customUI.portalDevice.needPortalKey"));
commandBuilder.set("#UsageErrorLabel.Text", Message.translation("server.customUI.portalDevice.nothingHeld"));
} else if (state == PortalDeviceSummonPage.Error.PORTAL_INSIDE_PORTAL) {
commandBuilder.set("#UsageErrorLabel.Text", Message.translation("server.customUI.portalDevice.portalInsidePortal"));
} else if (state == PortalDeviceSummonPage.Error.MAX_ACTIVE_PORTALS) {
commandBuilder.set("#UsageErrorLabel.Text", Message.translation("server.customUI.portalDevice.maxFragments").param("max", 4));
} else if (state instanceof PortalDeviceSummonPage.InstanceKeyNotFound(String wisdomKeys)) {
commandBuilder.set(
"#UsageErrorLabel.Text", "The instance id '" + wisdomKeys + "' does not exist, this is a developer error with the portaltype."
);
} else if (state instanceof PortalDeviceSummonPage.PortalTypeNotFound(String var24)) {
commandBuilder.set("#UsageErrorLabel.Text", "The portaltype id '" + var24 + "' does not exist, this is a developer error with the portal key.");
} else if (state == PortalDeviceSummonPage.Error.BOTCHED_GAMEPLAY_CONFIG) {
commandBuilder.set(
"#UsageErrorLabel.Text",
"The gameplay config set on the PortalType set in the key does not have a Portal plugin configuration, this is a developer error."
);
} else {
commandBuilder.set("#UsageErrorLabel.Text", Message.translation("server.customUI.portalDevice.unknownError").param("state", state.toString()));
}
}
}
}
private static void updateCustomPills(UICommandBuilder commandBuilder, PortalType portalType) {
List<PillTag> pills = portalType.getDescription().getPillTags();
for (int i = 0; i < pills.size(); i++) {
PillTag pillTag = pills.get(i);
String child = "#Pills[" + i + "]";
commandBuilder.append("#Pills", "Pages/Portals/Pill.ui");
commandBuilder.set(child + ".Background.Color", ColorParseUtil.colorToHexString(pillTag.getColor()));
commandBuilder.set(child + " #Label.TextSpans", pillTag.getMessage());
}
}
private static void updateBulletList(UICommandBuilder commandBuilder, String selector, String[] messageKeys) {
for (int i = 0; i < messageKeys.length; i++) {
String messageKey = messageKeys[i];
String child = selector + "[" + i + "]";
commandBuilder.append(selector, "Pages/Portals/BulletPoint.ui");
commandBuilder.set(child + " #Label.TextSpans", Message.translation(messageKey));
}
}
public static Message createDescription(PortalType portalType, int timeLimitSeconds) {
Message msg = Message.empty();
Message durationMsg = formatDurationCrudely(timeLimitSeconds);
msg.insert(Message.translation("server.customUI.portalDevice.timeLimit").param("limit", durationMsg.color("#f9cb13")));
return msg;
}
private static Message formatDurationCrudely(int seconds) {
if (seconds < 0) {
return Message.translation("server.customUI.portalDevice.durationUnlimited");
} else if (seconds >= 120) {
int minutes = seconds / 60;
return Message.translation("server.customUI.portalDevice.durationMinutes").param("duration", minutes);
} else {
return Message.translation("server.customUI.portalDevice.durationSeconds").param("duration", seconds);
}
}
public void handleDataEvent(@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store, @Nonnull PortalDeviceSummonPage.Data data) {
Player playerComponent = store.getComponent(ref, Player.getComponentType());
assert playerComponent != null;
if (this.computeState(playerComponent, store) instanceof PortalDeviceSummonPage.CanSpawnPortal canSpawn) {
if ("SummonMouseEntered".equals(data.action)) {
UICommandBuilder commandBuilder = new UICommandBuilder();
commandBuilder.set("#Vignette.Visible", true);
this.sendUpdate(commandBuilder, null, false);
} else if ("SummonMouseExited".equals(data.action)) {
UICommandBuilder commandBuilder = new UICommandBuilder();
commandBuilder.set("#Vignette.Visible", false);
this.sendUpdate(commandBuilder, null, false);
} else {
playerComponent.getPageManager().setPage(ref, store, Page.None);
World originWorld = store.getExternalData().getWorld();
int index = canSpawn.blockState().getIndex();
int x = ChunkUtil.xFromBlockInColumn(index);
int y = ChunkUtil.yFromBlockInColumn(index);
int z = ChunkUtil.zFromBlockInColumn(index);
WorldChunk worldChunk = canSpawn.worldChunk();
PortalKey portalKey = canSpawn.portalKey();
PortalDevice portalDevice = canSpawn.portalDevice();
BlockType blockType = worldChunk.getBlockType(x, y, z);
if (blockType == portalDevice.getBaseBlockType()) {
if (this.config.areBlockStatesValid(blockType)) {
int rotation = worldChunk.getRotationIndex(x, y, z);
BlockType spawningType = blockType.getBlockForState(this.config.getSpawningState());
BlockType onType = blockType.getBlockForState(this.config.getOnState());
BlockType offType = blockType.getBlockForState(this.config.getOffState());
int setting = 6;
worldChunk.setBlock(x, y, z, BlockType.getAssetMap().getIndex(spawningType.getId()), spawningType, rotation, 0, 6);
double worldX = ChunkUtil.worldCoordFromLocalCoord(worldChunk.getX(), x) + 0.5;
double worldY = y + 0.5;
double worldZ = ChunkUtil.worldCoordFromLocalCoord(worldChunk.getZ(), z) + 0.5;
if (spawningType.getInteractionSoundEventIndex() != 0) {
SoundUtil.playSoundEvent3d(spawningType.getInteractionSoundEventIndex(), SoundCategory.SFX, worldX, worldY, worldZ, store);
}
decrementItemInHand(playerComponent.getInventory(), 1);
Transform transform = new Transform(x + 0.5, y + 1.0, z + 0.5);
UUIDComponent uuidComponent = store.getComponent(ref, UUIDComponent.getComponentType());
assert uuidComponent != null;
PortalType portalType = canSpawn.portalType;
UUID playerUUID = uuidComponent.getUuid();
PortalGameplayConfig gameplayConfig = canSpawn.portalGameplayConfig;
InstancesPlugin.get()
.spawnInstance(portalType.getInstanceId(), originWorld, transform)
.thenCompose(spawnedWorld -> {
WorldConfig worldConfig = spawnedWorld.getWorldConfig();
worldConfig.setDeleteOnUniverseStart(true);
worldConfig.setDeleteOnRemove(true);
worldConfig.setGameplayConfig(portalType.getGameplayConfigId());
InstanceWorldConfig instanceConfig = InstanceWorldConfig.ensureAndGet(worldConfig);
if (instanceConfig.getDiscovery() == null) {
InstanceDiscoveryConfig discoveryConfig = new InstanceDiscoveryConfig();
discoveryConfig.setTitleKey(portalType.getDescription().getDisplayNameKey());
discoveryConfig.setSubtitleKey("server.portals.discoverySubtitle");
discoveryConfig.setDisplay(true);
discoveryConfig.setAlwaysDisplay(true);
instanceConfig.setDiscovery(discoveryConfig);
}
PortalRemovalCondition portalRemoval = new PortalRemovalCondition(portalKey.getTimeLimitSeconds());
instanceConfig.setRemovalConditions(portalRemoval);
PortalWorld portalWorld = spawnedWorld.getEntityStore().getStore().getResource(PortalWorld.getResourceType());
portalWorld.init(portalType, portalKey.getTimeLimitSeconds(), portalRemoval, gameplayConfig);
String returnBlockType = portalDevice.getConfig().getReturnBlock();
if (returnBlockType == null) {
throw new RuntimeException("Return block type on PortalDevice is misconfigured");
} else {
return spawnReturnPortal(spawnedWorld, portalWorld, playerUUID, returnBlockType);
}
})
.thenAcceptAsync(spawnedWorld -> {
portalDevice.setDestinationWorld(spawnedWorld);
worldChunk.setBlock(x, y, z, BlockType.getAssetMap().getIndex(onType.getId()), onType, rotation, 0, 6);
}, originWorld)
.exceptionallyAsync(
t -> {
playerComponent.sendMessage(Message.translation("server.portals.device.internalErrorSpawning"));
((HytaleLogger.Api)HytaleLogger.getLogger().at(Level.SEVERE).withCause(t))
.log("Error creating instance for Portal Device " + portalKey, t);
worldChunk.setBlock(x, y, z, BlockType.getAssetMap().getIndex(offType.getId()), offType, rotation, 0, 6);
return null;
},
originWorld
);
}
}
}
}
}
private static CompletableFuture<World> spawnReturnPortal(World world, PortalWorld portalWorld, UUID sampleUuid, String portalBlockType) {
PortalSpawn portalSpawn = portalWorld.getPortalType().getPortalSpawn();
return getSpawnTransform(world, sampleUuid, portalSpawn)
.thenCompose(
spawnTransform -> {
Vector3d spawnPoint = spawnTransform.getPosition();
return world.getChunkAsync(ChunkUtil.indexChunkFromBlock((int)spawnPoint.x, (int)spawnPoint.z))
.thenAccept(
chunk -> {
for (int dy = 0; dy < 3; dy++) {
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
chunk.setBlock((int)spawnPoint.x + dx, (int)spawnPoint.y + dy, (int)spawnPoint.z + dz, BlockType.EMPTY);
}
}
}
chunk.setBlock((int)spawnPoint.x, (int)spawnPoint.y, (int)spawnPoint.z, portalBlockType);
portalWorld.setSpawnPoint(spawnTransform);
world.getWorldConfig().setSpawnProvider(new IndividualSpawnProvider(spawnTransform));
HytaleLogger.getLogger()
.at(Level.INFO)
.log(
"Spawned return portal for " + world.getName() + " at " + (int)spawnPoint.x + ", " + (int)spawnPoint.y + ", " + (int)spawnPoint.z
);
}
)
.thenApply(nothing -> world);
}
);
}
private static CompletableFuture<Transform> getSpawnTransform(World world, UUID sampleUuid, @Nullable PortalSpawn portalSpawn) {
ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider();
if (spawnProvider == null) {
return CompletableFuture.completedFuture(null);
} else {
Transform worldSpawnPoint = spawnProvider.getSpawnPoint(world, sampleUuid);
return DEFAULT_WORLDGEN_SPAWN.equals(worldSpawnPoint) && portalSpawn != null ? CompletableFuture.supplyAsync(() -> {
Transform computedSpawn = PortalSpawnFinder.computeSpawnTransform(world, portalSpawn);
return computedSpawn == null ? worldSpawnPoint : computedSpawn;
}, world) : CompletableFuture.completedFuture(worldSpawnPoint);
}
}
private PortalDeviceSummonPage.State computeState(@Nonnull Player player, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
if (!this.blockRef.isValid()) {
return PortalDeviceSummonPage.Error.INVALID_BLOCK;
} else {
int activeFragments = PortalsPlugin.getInstance().countActiveFragments();
if (activeFragments >= 4) {
return PortalDeviceSummonPage.Error.MAX_ACTIVE_PORTALS;
} else {
Store<ChunkStore> chunkStore = this.blockRef.getStore();
BlockModule.BlockStateInfo blockStateInfo = chunkStore.getComponent(this.blockRef, BlockModule.BlockStateInfo.getComponentType());
PortalDevice portalDevice = chunkStore.getComponent(this.blockRef, PortalDevice.getComponentType());
if (blockStateInfo != null && portalDevice != null) {
Ref<ChunkStore> chunkRef = blockStateInfo.getChunkRef();
if (chunkRef != null && chunkRef.isValid()) {
WorldChunk worldChunk = chunkStore.getComponent(chunkRef, WorldChunk.getComponentType());
if (worldChunk == null) {
return PortalDeviceSummonPage.Error.INVALID_BLOCK;
} else {
World existingDestinationWorld = portalDevice.getDestinationWorld();
if (existingDestinationWorld != null) {
return PortalDeviceSummonPage.Error.INVALID_DESTINATION;
} else if (this.offeredItemStack == null) {
return PortalDeviceSummonPage.Error.NOTHING_OFFERED;
} else {
ItemStack inHand = player.getInventory().getItemInHand();
if (!this.offeredItemStack.equals(inHand)) {
return PortalDeviceSummonPage.Error.OFFERED_IS_NOT_HELD;
} else {
Item offeredItem = this.offeredItemStack.getItem();
PortalKey portalKey = offeredItem.getPortalKey();
if (portalKey == null) {
return PortalDeviceSummonPage.Error.NOT_A_PORTAL_KEY;
} else {
String portalTypeId = portalKey.getPortalTypeId();
PortalType portalType = PortalType.getAssetMap().getAsset(portalTypeId);
if (portalType == null) {
return new PortalDeviceSummonPage.PortalTypeNotFound(portalTypeId);
} else {
String instanceId = portalType.getInstanceId();
InstancesPlugin.get();
boolean instanceExists = InstancesPlugin.doesInstanceAssetExist(instanceId);
if (!instanceExists) {
return new PortalDeviceSummonPage.InstanceKeyNotFound(instanceId);
} else {
PortalWorld insidePortalWorld = componentAccessor.getResource(PortalWorld.getResourceType());
if (insidePortalWorld.exists()) {
return PortalDeviceSummonPage.Error.PORTAL_INSIDE_PORTAL;
} else {
String gameplayConfigId = portalType.getGameplayConfigId();
GameplayConfig gameplayConfig = GameplayConfig.getAssetMap().getAsset(gameplayConfigId);
PortalGameplayConfig portalGameplayConfig = gameplayConfig == null
? null
: gameplayConfig.getPluginConfig().get(PortalGameplayConfig.class);
return (PortalDeviceSummonPage.State)(portalGameplayConfig == null
? PortalDeviceSummonPage.Error.BOTCHED_GAMEPLAY_CONFIG
: new PortalDeviceSummonPage.CanSpawnPortal(
portalKey, portalType, worldChunk, blockStateInfo, portalDevice, portalGameplayConfig
));
}
}
}
}
}
}
}
} else {
return PortalDeviceSummonPage.Error.INVALID_BLOCK;
}
} else {
return PortalDeviceSummonPage.Error.INVALID_BLOCK;
}
}
}
}
private static void decrementItemInHand(Inventory inventory, int amount) {
if (!inventory.usingToolsItem()) {
byte hotbarSlot = inventory.getActiveHotbarSlot();
if (hotbarSlot != -1) {
ItemContainer hotbar = inventory.getHotbar();
ItemStack inHand = hotbar.getItemStack(hotbarSlot);
if (inHand != null) {
hotbar.removeItemStackFromSlot(hotbarSlot, inHand, amount, false, true);
}
}
}
}
private record CanSpawnPortal(
PortalKey portalKey,
PortalType portalType,
WorldChunk worldChunk,
BlockModule.BlockStateInfo blockState,
PortalDevice portalDevice,
PortalGameplayConfig portalGameplayConfig
) implements PortalDeviceSummonPage.State {
}
protected static class Data {
private static final String KEY_ACTION = "Action";
public static final BuilderCodec<PortalDeviceSummonPage.Data> CODEC = BuilderCodec.builder(
PortalDeviceSummonPage.Data.class, PortalDeviceSummonPage.Data::new
)
.append(new KeyedCodec<>("Action", Codec.STRING), (entry, s) -> entry.action = s, entry -> entry.action)
.add()
.build();
private String action;
}
private static enum Error implements PortalDeviceSummonPage.State {
NOTHING_OFFERED,
OFFERED_IS_NOT_HELD,
NOT_A_PORTAL_KEY,
INVALID_BLOCK,
INVALID_DESTINATION,
PORTAL_INSIDE_PORTAL,
BOTCHED_GAMEPLAY_CONFIG,
MAX_ACTIVE_PORTALS;
}
private record InstanceKeyNotFound(String instanceId) implements PortalDeviceSummonPage.State {
}
private record PortalTypeNotFound(String portalTypeId) implements PortalDeviceSummonPage.State {
}
private sealed interface State
permits PortalDeviceSummonPage.CanSpawnPortal,
PortalDeviceSummonPage.Error,
PortalDeviceSummonPage.InstanceKeyNotFound,
PortalDeviceSummonPage.PortalTypeNotFound {
}
}