package com.hypixel.hytale.server.npc.pages; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.common.util.StringCompareUtil; import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.NonSerialized; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.RemoveReason; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.math.vector.Transform; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.math.vector.Vector3f; import com.hypixel.hytale.math.vector.Vector3i; 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.item.config.Item; import com.hypixel.hytale.server.core.asset.type.model.config.Model; import com.hypixel.hytale.server.core.asset.type.model.config.ModelAsset; import com.hypixel.hytale.server.core.command.system.exceptions.GeneralCommandException; import com.hypixel.hytale.server.core.entity.UUIDComponent; import com.hypixel.hytale.server.core.entity.entities.BlockEntity; 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.ItemStack; import com.hypixel.hytale.server.core.modules.entity.component.EntityScaleComponent; import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation; import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent; import com.hypixel.hytale.server.core.modules.entity.component.PersistentModel; import com.hypixel.hytale.server.core.modules.entity.component.PropComponent; import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.modules.entity.item.ItemComponent; import com.hypixel.hytale.server.core.modules.entity.item.PreventItemMerging; import com.hypixel.hytale.server.core.modules.entity.item.PreventPickup; import com.hypixel.hytale.server.core.modules.entity.tracker.NetworkId; import com.hypixel.hytale.server.core.ui.ItemGridSlot; import com.hypixel.hytale.server.core.ui.Value; 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.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.core.util.TargetUtil; import com.hypixel.hytale.server.npc.NPCPlugin; import com.hypixel.hytale.server.npc.asset.builder.Builder; import com.hypixel.hytale.server.npc.asset.builder.BuilderInfo; import com.hypixel.hytale.server.npc.role.Role; import com.hypixel.hytale.server.spawning.ISpawnableWithModel; import com.hypixel.hytale.server.spawning.SpawningContext; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.Map.Entry; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class EntitySpawnPage extends InteractiveCustomUIPage { private static final String COMMON_TEXT_BUTTON_DOCUMENT = "Common/TextButton.ui"; private static final String COMMON_UI_DOCUMENT = "Common.ui"; private static final Value BUTTON_LABEL_STYLE = Value.ref("Common/TextButton.ui", "LabelStyle"); private static final Value BUTTON_LABEL_STYLE_SELECTED = Value.ref("Common/TextButton.ui", "SelectedLabelStyle"); private static final Value TAB_STYLE_ACTIVE = Value.ref("Common.ui", "DefaultTextButtonStyle"); private static final Value TAB_STYLE_INACTIVE = Value.ref("Common.ui", "SecondaryTextButtonStyle"); private static final String TAB_NPC = "NPC"; private static final String TAB_ITEMS = "Items"; private static final String TAB_MODEL = "Model"; private static final String KEY_SELECT_AN_ITEM = "server.customUI.entitySpawnPage.selectAnItem"; private static final String KEY_SELECT_AN_NPC = "server.customUI.entitySpawnPage.selectAnNpc"; private static final String KEY_SELECT_A_MODEL = "server.customUI.entitySpawnPage.selectAModel"; private static final int MAX_SPAWN_COUNT = 100; private static final float BLOCK_ENTITY_BASE_SCALE = 2.0F; private static final int LOOK_RAYCAST_DISTANCE = 4; private static final int FALLBACK_RAYCAST_DOWN_DISTANCE = 3; private static final double FALLBACK_RAYCAST_Y_OFFSET = 0.5; @Nonnull private String activeTab = "NPC"; @Nonnull private String searchQuery = ""; private List npcRoles; @Nullable private String selectedNpcRole; private List modelIds; @Nullable private String selectedModelId; @Nullable private String selectedItemId; @Nullable private Ref modelPreview; private Vector3d position; private Vector3f rotation; private float currentRotationOffset = 0.0F; private float currentScale = 1.0F; private float lastPreviewScale = 1.0F; private long lastScaleUpdateTime = 0L; public EntitySpawnPage(@Nonnull PlayerRef playerRef) { super(playerRef, CustomPageLifetime.CanDismiss, EntitySpawnPage.EntitySpawnPageEventData.CODEC); } @Override public void build( @Nonnull Ref ref, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder, @Nonnull Store store ) { commandBuilder.append("Pages/EntitySpawnPage.ui"); eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#SearchInput", EventData.of("@SearchQuery", "#SearchInput.Value"), false); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#RotationOffset", new EventData().append("Type", "UpdateRotationOffset").append("@RotationOffset", "#RotationOffset.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#ScaleSlider", new EventData().append("Type", "UpdateScale").append("@Scale", "#ScaleSlider.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.MouseButtonReleased, "#ScaleSlider", new EventData().append("Type", "ScaleReleased").append("@Scale", "#ScaleSlider.Value"), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, "#Spawn", new EventData().append("Type", "Spawn").append("@Count", "#Count.Value").append("@Scale", "#ScaleSlider.Value"), false ); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#TabNPC", new EventData().append("Type", "TabSwitch").append("Tab", "NPC"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#TabItems", new EventData().append("Type", "TabSwitch").append("Tab", "Items"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#TabModel", new EventData().append("Type", "TabSwitch").append("Tab", "Model"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.Dropped, "#ItemMaterialSlot", new EventData().append("Type", "SetItemMaterial"), false); eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#ClearMaterial", new EventData().append("Type", "ClearMaterial"), false); this.buildList(ref, store, commandBuilder, eventBuilder); } public void handleDataEvent(@Nonnull Ref ref, @Nonnull Store store, @Nonnull EntitySpawnPage.EntitySpawnPageEventData data) { if (data.searchQuery != null) { this.searchQuery = data.searchQuery.trim().toLowerCase(); UICommandBuilder commandBuilder = new UICommandBuilder(); UIEventBuilder eventBuilder = new UIEventBuilder(); this.buildList(ref, store, commandBuilder, eventBuilder); this.sendUpdate(commandBuilder, eventBuilder, false); } else { String var9 = data.type; switch (var9) { case "TabSwitch": if (data.tab != null && !data.tab.equals(this.activeTab)) { this.activeTab = data.tab; this.searchQuery = ""; this.clearPreview(store); UICommandBuilder commandBuilder = new UICommandBuilder(); UIEventBuilder eventBuilder = new UIEventBuilder(); this.updateTabVisibility(commandBuilder); commandBuilder.set("#SearchInput.Value", ""); this.buildList(ref, store, commandBuilder, eventBuilder); this.sendUpdate(commandBuilder, eventBuilder, false); } break; case "Select": this.handleSelect(ref, store, data); break; case "SetItemMaterial": this.handleSetItemMaterial(ref, store, data); break; case "ClearMaterial": this.clearSelectedItem(ref, store); break; case "UpdateRotationOffset": this.currentRotationOffset = (float)Math.toRadians(data.rotationOffset); if (this.modelPreview != null && this.modelPreview.isValid()) { TransformComponent transform = store.getComponent(this.modelPreview, TransformComponent.getComponentType()); transform.getRotation().setYaw(this.rotation.getYaw() + this.currentRotationOffset); HeadRotation headRotation = store.getComponent(this.modelPreview, HeadRotation.getComponentType()); if (headRotation != null) { headRotation.getRotation().setYaw(this.rotation.getYaw() + this.currentRotationOffset); } } break; case "UpdateScale": if (data.scale != null && data.scale >= 0.1F) { this.currentScale = data.scale; UICommandBuilder commandBuilder = new UICommandBuilder(); commandBuilder.set("#ScaleValue.Text", String.format("%.1f", this.currentScale)); this.sendUpdate(commandBuilder, null, false); long now = System.currentTimeMillis(); if (this.modelPreview != null && this.modelPreview.isValid() && this.currentScale != this.lastPreviewScale && now - this.lastScaleUpdateTime >= 200L) { this.lastScaleUpdateTime = now; this.lastPreviewScale = this.currentScale; this.updatePreviewScale(ref, store); } } break; case "ScaleReleased": if (data.scale != null && data.scale >= 0.1F) { this.currentScale = data.scale; if ("Model".equals(this.activeTab) && this.selectedModelId != null) { ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset(this.selectedModelId); if (modelAsset != null) { UICommandBuilder commandBuilder = new UICommandBuilder(); this.createOrUpdatePreview(ref, store, commandBuilder, Model.createStaticScaledModel(modelAsset, this.currentScale)); } } else if ("Items".equals(this.activeTab) && this.selectedItemId != null) { Item item = Item.getAssetMap().getAsset(this.selectedItemId); if (item != null) { Model model = this.getItemModel(item); if (model != null) { UICommandBuilder commandBuilder = new UICommandBuilder(); this.createOrUpdatePreview(ref, store, commandBuilder, model); } else if (item.hasBlockType()) { this.createOrUpdateBlockPreview(ref, store, this.selectedItemId); } else { this.createOrUpdateItemPreview(ref, store, this.selectedItemId); } } } } break; case "Spawn": this.handleSpawn(ref, store, data); } } } private void handleSelect(@Nonnull Ref ref, @Nonnull Store store, @Nonnull EntitySpawnPage.EntitySpawnPageEventData data) { UICommandBuilder commandBuilder = new UICommandBuilder(); String var5 = this.activeTab; switch (var5) { case "NPC": if (data.npcRole != null) { this.selectNPCRole(ref, store, data.npcRole, commandBuilder); } break; case "Items": if (data.itemId != null) { this.selectItem(ref, store, data.itemId, commandBuilder); } break; case "Model": if (data.modelId != null) { this.selectModel(ref, store, data.modelId, commandBuilder); } } this.sendUpdate(commandBuilder, null, false); } private void handleSetItemMaterial(@Nonnull Ref ref, @Nonnull Store store, @Nonnull EntitySpawnPage.EntitySpawnPageEventData data) { if (data.itemStackId != null) { UICommandBuilder commandBuilder = new UICommandBuilder(); this.selectItem(ref, store, data.itemStackId, commandBuilder); this.sendUpdate(commandBuilder, null, false); } } private void clearSelectedItem(@Nonnull Ref ref, @Nonnull Store store) { this.selectedItemId = null; this.clearPreview(store); UICommandBuilder commandBuilder = new UICommandBuilder(); commandBuilder.set("#SelectedName.Text", Message.translation("server.customUI.entitySpawnPage.selectAnItem")); commandBuilder.set("#ItemMaterialSlot.Slots", new ItemGridSlot[]{new ItemGridSlot()}); commandBuilder.set("#ClearMaterial.Visible", false); commandBuilder.set("#DropIndicator.Visible", true); this.sendUpdate(commandBuilder, null, false); } private void handleSpawn(@Nonnull Ref ref, @Nonnull Store store, @Nonnull EntitySpawnPage.EntitySpawnPageEventData data) { String var4 = this.activeTab; switch (var4) { case "NPC": this.spawnNPC(ref, store, data.count); break; case "Items": this.spawnItem(ref, store, data.count); break; case "Model": this.spawnModel(ref, store, data.count); } } private void spawnNPC(@Nonnull Ref ref, @Nonnull Store store, int count) { if (this.selectedNpcRole != null && this.position != null && this.rotation != null) { if (count >= 1 && count <= 100) { this.clearPreview(store); Vector3f spawnRotation = this.rotation.clone(); spawnRotation.setYaw(this.rotation.getYaw() + this.currentRotationOffset); for (int i = 0; i < count; i++) { NPCPlugin.get().spawnNPC(store, this.selectedNpcRole, null, this.position, spawnRotation); } store.getComponent(ref, Player.getComponentType()).getPageManager().setPage(ref, store, Page.None); this.playerRef .sendMessage( Message.translation(count == 1 ? "server.npc.spawn.spawnedOne" : "server.npc.spawn.spawnedMany") .param("quantity", count) .param("type", this.selectedNpcRole) ); } } } private void spawnModel(@Nonnull Ref ref, @Nonnull Store store, int count) { if (this.selectedModelId != null && this.position != null && this.rotation != null) { if (count >= 1 && count <= 100) { this.clearPreview(store); ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset(this.selectedModelId); if (modelAsset != null) { Model model = Model.createStaticScaledModel(modelAsset, this.currentScale); Vector3f spawnRotation = this.rotation.clone(); spawnRotation.setYaw(this.rotation.getYaw() + this.currentRotationOffset); for (int i = 0; i < count; i++) { Holder holder = store.getRegistry().newHolder(); holder.addComponent(NetworkId.getComponentType(), new NetworkId(store.getExternalData().takeNextNetworkId())); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, spawnRotation)); holder.addComponent(ModelComponent.getComponentType(), new ModelComponent(model)); holder.addComponent( PersistentModel.getComponentType(), new PersistentModel(new Model.ModelReference(this.selectedModelId, this.currentScale, null, true)) ); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(spawnRotation)); holder.addComponent(PropComponent.getComponentType(), PropComponent.get()); holder.ensureComponent(UUIDComponent.getComponentType()); store.addEntity(holder, AddReason.SPAWN); } store.getComponent(ref, Player.getComponentType()).getPageManager().setPage(ref, store, Page.None); this.playerRef .sendMessage( Message.translation("server.customUI.entitySpawnPage.spawnedModel").param("quantity", count).param("model", this.selectedModelId) ); } } } } @Override public void onDismiss(@Nonnull Ref ref, @Nonnull Store store) { this.clearPreview(store); } private void clearPreview(@Nonnull Store store) { if (this.modelPreview != null && this.modelPreview.isValid()) { store.removeEntity(this.modelPreview, RemoveReason.REMOVE); } this.modelPreview = null; } private void updateTabVisibility(@Nonnull UICommandBuilder commandBuilder) { commandBuilder.set("#NPCContent.Visible", this.activeTab.equals("NPC")); commandBuilder.set("#ItemsContent.Visible", this.activeTab.equals("Items")); commandBuilder.set("#ModelContent.Visible", this.activeTab.equals("Model")); commandBuilder.set("#TabNPC.Style", this.activeTab.equals("NPC") ? TAB_STYLE_ACTIVE : TAB_STYLE_INACTIVE); commandBuilder.set("#TabItems.Style", this.activeTab.equals("Items") ? TAB_STYLE_ACTIVE : TAB_STYLE_INACTIVE); commandBuilder.set("#TabModel.Style", this.activeTab.equals("Model") ? TAB_STYLE_ACTIVE : TAB_STYLE_INACTIVE); commandBuilder.set("#RotationGroup.Visible", this.activeTab.equals("Model") || this.activeTab.equals("NPC") || this.activeTab.equals("Items")); commandBuilder.set("#ScaleGroup.Visible", this.activeTab.equals("Model") || this.activeTab.equals("Items")); if (this.activeTab.equals("Model") || this.activeTab.equals("Items")) { commandBuilder.set("#ScaleSlider.Value", this.currentScale); commandBuilder.set("#ScaleValue.Text", String.format("%.1f", this.currentScale)); } } private void buildList( @Nonnull Ref ref, @Nonnull Store store, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder ) { this.updateTabVisibility(commandBuilder); String var5 = this.activeTab; switch (var5) { case "NPC": this.buildNPCList(ref, store, commandBuilder, eventBuilder); break; case "Items": this.buildItemsContent(ref, store, commandBuilder); break; case "Model": this.buildModelList(ref, store, commandBuilder, eventBuilder); } } private void buildNPCList( @Nonnull Ref ref, @Nonnull Store store, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder ) { commandBuilder.clear("#NPCList"); List roleTemplateNames = NPCPlugin.get().getRoleTemplateNames(true); if (!this.searchQuery.isEmpty()) { Object2IntMap map = new Object2IntOpenHashMap(roleTemplateNames.size()); for (String value : roleTemplateNames) { int fuzzyDistance = StringCompareUtil.getFuzzyDistance(value, this.searchQuery, Locale.ENGLISH); if (fuzzyDistance > 0) { map.put(value, fuzzyDistance); } } this.npcRoles = map.keySet().stream().sorted().sorted(Comparator.comparingInt(map::getInt).reversed()).limit(20L).collect(Collectors.toList()); } else { roleTemplateNames.sort(String::compareTo); this.npcRoles = roleTemplateNames; } int i = 0; for (int bound = this.npcRoles.size(); i < bound; i++) { String id = this.npcRoles.get(i); String selector = "#NPCList[" + i + "]"; commandBuilder.append("#NPCList", "Common/TextButton.ui"); commandBuilder.set(selector + " #Button.Text", id); eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, selector + " #Button", new EventData().append("Type", "Select").append("NPCRole", id), false ); } if (!this.npcRoles.isEmpty() && this.selectedNpcRole != null) { if (this.npcRoles.contains(this.selectedNpcRole)) { this.selectNPCRole(ref, store, this.selectedNpcRole, commandBuilder); } else { this.selectedNpcRole = null; this.clearPreview(store); commandBuilder.set("#SelectedName.Text", Message.translation("server.customUI.entitySpawnPage.selectAnNpc")); } } else if (this.selectedNpcRole == null) { commandBuilder.set("#SelectedName.Text", Message.translation("server.customUI.entitySpawnPage.selectAnNpc")); } } private void buildModelList( @Nonnull Ref ref, @Nonnull Store store, @Nonnull UICommandBuilder commandBuilder, @Nonnull UIEventBuilder eventBuilder ) { commandBuilder.clear("#ModelList"); Set itemAndBlockModelPaths = new HashSet<>(); for (Item item : Item.getAssetMap().getAssetMap().values()) { if (item.getModel() != null) { itemAndBlockModelPaths.add(item.getModel()); } } for (BlockType blockType : BlockType.getAssetMap().getAssetMap().values()) { if (blockType.getCustomModel() != null) { itemAndBlockModelPaths.add(blockType.getCustomModel()); } } Set allModels = new HashSet<>(); for (Entry entry : ModelAsset.getAssetMap().getAssetMap().entrySet()) { String modelPath = entry.getValue().getModel(); if (modelPath != null && !itemAndBlockModelPaths.contains(modelPath) && !modelPath.startsWith("Items/Projectiles/")) { allModels.add(entry.getKey()); } } if (!this.searchQuery.isEmpty()) { Object2IntMap map = new Object2IntOpenHashMap(allModels.size()); for (String value : allModels) { int fuzzyDistance = StringCompareUtil.getFuzzyDistance(value, this.searchQuery, Locale.ENGLISH); if (fuzzyDistance > 0) { map.put(value, fuzzyDistance); } } this.modelIds = map.keySet().stream().sorted().sorted(Comparator.comparingInt(map::getInt).reversed()).limit(20L).collect(Collectors.toList()); } else { this.modelIds = allModels.stream().sorted().collect(Collectors.toList()); } int i = 0; for (int bound = this.modelIds.size(); i < bound; i++) { String id = this.modelIds.get(i); String selector = "#ModelList[" + i + "]"; commandBuilder.append("#ModelList", "Common/TextButton.ui"); commandBuilder.set(selector + " #Button.Text", id); eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, selector + " #Button", new EventData().append("Type", "Select").append("ModelId", id), false ); } if (!this.modelIds.isEmpty() && this.selectedModelId != null) { if (this.modelIds.contains(this.selectedModelId)) { this.selectModel(ref, store, this.selectedModelId, commandBuilder); } else { this.selectedModelId = null; this.clearPreview(store); commandBuilder.set("#SelectedName.Text", Message.translation("server.customUI.entitySpawnPage.selectAModel")); } } else if (this.selectedModelId == null) { commandBuilder.set("#SelectedName.Text", Message.translation("server.customUI.entitySpawnPage.selectAModel")); } } private void buildItemsContent(@Nonnull Ref ref, @Nonnull Store store, @Nonnull UICommandBuilder commandBuilder) { if (this.selectedItemId != null) { Item item = Item.getAssetMap().getAsset(this.selectedItemId); if (item != null) { commandBuilder.set("#SelectedName.Text", this.selectedItemId); commandBuilder.set("#ItemMaterialSlot.Slots", new ItemGridSlot[]{new ItemGridSlot(new ItemStack(this.selectedItemId, 1))}); commandBuilder.set("#ClearMaterial.Visible", true); commandBuilder.set("#DropIndicator.Visible", false); } } else { commandBuilder.set("#SelectedName.Text", Message.translation("server.customUI.entitySpawnPage.selectAnItem")); commandBuilder.set("#ItemMaterialSlot.Slots", new ItemGridSlot[]{new ItemGridSlot()}); commandBuilder.set("#ClearMaterial.Visible", false); commandBuilder.set("#DropIndicator.Visible", true); } } private void selectItem(@Nonnull Ref ref, @Nonnull Store store, @Nonnull String itemId, @Nonnull UICommandBuilder commandBuilder) { Item item = Item.getAssetMap().getAsset(itemId); if (item != null) { this.selectedItemId = itemId; commandBuilder.set("#SelectedName.Text", itemId); commandBuilder.set("#ItemMaterialSlot.Slots", new ItemGridSlot[]{new ItemGridSlot(new ItemStack(itemId, 1))}); commandBuilder.set("#ClearMaterial.Visible", true); commandBuilder.set("#DropIndicator.Visible", false); Model model = this.getItemModel(item); if (model != null) { this.createOrUpdatePreview(ref, store, commandBuilder, model); } else if (item.hasBlockType()) { this.createOrUpdateBlockPreview(ref, store, itemId); } else { this.createOrUpdateItemPreview(ref, store, itemId); } } } private void spawnItem(@Nonnull Ref ref, @Nonnull Store store, int count) { if (this.selectedItemId != null && this.position != null && this.rotation != null) { if (count >= 1 && count <= 100) { Item item = Item.getAssetMap().getAsset(this.selectedItemId); if (item != null) { this.clearPreview(store); Vector3f spawnRotation = this.rotation.clone(); spawnRotation.setYaw(this.rotation.getYaw() + this.currentRotationOffset); Model model = this.getItemModel(item); if (model != null) { String modelId = this.getItemModelId(item); for (int i = 0; i < count; i++) { Holder holder = store.getRegistry().newHolder(); holder.addComponent(NetworkId.getComponentType(), new NetworkId(store.getExternalData().takeNextNetworkId())); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, spawnRotation)); holder.addComponent(ModelComponent.getComponentType(), new ModelComponent(model)); holder.addComponent( PersistentModel.getComponentType(), new PersistentModel(new Model.ModelReference(modelId, this.currentScale, null, true)) ); ItemStack itemStack = new ItemStack(this.selectedItemId, 1); itemStack.setOverrideDroppedItemAnimation(true); holder.addComponent(ItemComponent.getComponentType(), new ItemComponent(itemStack)); holder.addComponent(PreventPickup.getComponentType(), PreventPickup.INSTANCE); holder.addComponent(PreventItemMerging.getComponentType(), PreventItemMerging.INSTANCE); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(spawnRotation)); holder.addComponent(PropComponent.getComponentType(), PropComponent.get()); holder.ensureComponent(UUIDComponent.getComponentType()); store.addEntity(holder, AddReason.SPAWN); } } else if (item.hasBlockType()) { for (int i = 0; i < count; i++) { Holder holder = store.getRegistry().newHolder(); holder.addComponent(BlockEntity.getComponentType(), new BlockEntity(this.selectedItemId)); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, spawnRotation)); holder.addComponent(EntityScaleComponent.getComponentType(), new EntityScaleComponent(this.currentScale * 2.0F)); ItemStack itemStack = new ItemStack(this.selectedItemId, 1); itemStack.setOverrideDroppedItemAnimation(true); holder.addComponent(ItemComponent.getComponentType(), new ItemComponent(itemStack)); holder.addComponent(PreventPickup.getComponentType(), PreventPickup.INSTANCE); holder.addComponent(PreventItemMerging.getComponentType(), PreventItemMerging.INSTANCE); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(spawnRotation)); holder.addComponent(PropComponent.getComponentType(), PropComponent.get()); holder.ensureComponent(UUIDComponent.getComponentType()); store.addEntity(holder, AddReason.SPAWN); } } else { for (int i = 0; i < count; i++) { Holder holder = store.getRegistry().newHolder(); holder.addComponent(NetworkId.getComponentType(), new NetworkId(store.getExternalData().takeNextNetworkId())); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, spawnRotation)); ItemStack itemStack = new ItemStack(this.selectedItemId, 1); itemStack.setOverrideDroppedItemAnimation(true); holder.addComponent(ItemComponent.getComponentType(), new ItemComponent(itemStack)); holder.addComponent(EntityScaleComponent.getComponentType(), new EntityScaleComponent(this.currentScale)); holder.addComponent(PreventPickup.getComponentType(), PreventPickup.INSTANCE); holder.addComponent(PreventItemMerging.getComponentType(), PreventItemMerging.INSTANCE); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(spawnRotation)); holder.addComponent(PropComponent.getComponentType(), PropComponent.get()); store.addEntity(holder, AddReason.SPAWN); } } store.getComponent(ref, Player.getComponentType()).getPageManager().setPage(ref, store, Page.None); this.playerRef .sendMessage(Message.translation("server.customUI.entitySpawnPage.spawnedItem").param("quantity", count).param("item", this.selectedItemId)); } } } } @Nullable private String getItemModelId(@Nonnull Item item) { String modelId = item.getModel(); if (modelId == null && item.hasBlockType()) { BlockType blockType = BlockType.getAssetMap().getAsset(item.getId()); if (blockType != null && blockType.getCustomModel() != null) { modelId = blockType.getCustomModel(); } } return modelId; } @Nullable private Model getItemModel(@Nonnull Item item) { String modelId = this.getItemModelId(item); if (modelId == null) { return null; } else { ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset(modelId); return modelAsset != null ? Model.createStaticScaledModel(modelAsset, this.currentScale) : null; } } private void selectNPCRole( @Nonnull Ref ref, @Nonnull Store store, @Nonnull String npcRole, @Nonnull UICommandBuilder commandBuilder ) { if (this.selectedNpcRole != null && this.npcRoles.contains(this.selectedNpcRole)) { commandBuilder.set("#NPCList[" + this.npcRoles.indexOf(this.selectedNpcRole) + "] #Button.Style", BUTTON_LABEL_STYLE); } commandBuilder.set("#NPCList[" + this.npcRoles.indexOf(npcRole) + "] #Button.Style", BUTTON_LABEL_STYLE_SELECTED); commandBuilder.set("#SelectedName.Text", npcRole); this.selectedNpcRole = npcRole; this.createOrUpdatePreview(ref, store, commandBuilder, this.getNPCModel()); } private void selectModel(@Nonnull Ref ref, @Nonnull Store store, @Nonnull String modelId, @Nonnull UICommandBuilder commandBuilder) { if (this.selectedModelId != null && this.modelIds.contains(this.selectedModelId)) { commandBuilder.set("#ModelList[" + this.modelIds.indexOf(this.selectedModelId) + "] #Button.Style", BUTTON_LABEL_STYLE); } commandBuilder.set("#ModelList[" + this.modelIds.indexOf(modelId) + "] #Button.Style", BUTTON_LABEL_STYLE_SELECTED); commandBuilder.set("#SelectedName.Text", modelId); this.selectedModelId = modelId; ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset(modelId); if (modelAsset != null) { this.createOrUpdatePreview(ref, store, commandBuilder, Model.createStaticScaledModel(modelAsset, this.currentScale)); } } private void initPosition(@Nonnull Ref ref, @Nonnull Store store) { TransformComponent transformComponent = store.getComponent(ref, TransformComponent.getComponentType()); assert transformComponent != null; HeadRotation headRotationComponent = store.getComponent(ref, HeadRotation.getComponentType()); assert headRotationComponent != null; Vector3d playerPosition = transformComponent.getPosition(); Vector3f headRotation = headRotationComponent.getRotation(); Vector3d direction = Transform.getDirection(headRotation.getPitch(), headRotation.getYaw()); Vector3d lookTarget = TargetUtil.getTargetLocation(ref, 4.0, store); Vector3d previewPosition; if (lookTarget != null) { previewPosition = lookTarget; } else { Vector3d aheadPosition = playerPosition.clone().add(direction.clone().scale(4.0)); World world = store.getExternalData().getWorld(); Vector3i groundTarget = TargetUtil.getTargetBlock( world, (blockId, fluidId) -> blockId != 0, aheadPosition.x, aheadPosition.y + 0.5, aheadPosition.z, 0.0, -1.0, 0.0, 3.0 ); if (groundTarget != null) { previewPosition = new Vector3d(groundTarget.x + 0.5, groundTarget.y + 1, groundTarget.z + 0.5); } else { previewPosition = aheadPosition; } } Vector3d relativePos = playerPosition.clone().subtract(previewPosition); relativePos.setY(0.0); Vector3f previewRotation = Vector3f.lookAt(relativePos); this.position = previewPosition; this.rotation = previewRotation; this.currentRotationOffset = 0.0F; } private void createOrUpdatePreview( @Nonnull Ref ref, @Nonnull Store store, @Nonnull UICommandBuilder commandBuilder, @Nullable Model model ) { if (model != null) { if (this.modelPreview != null && this.modelPreview.isValid()) { store.putComponent(this.modelPreview, ModelComponent.getComponentType(), new ModelComponent(model)); } else { this.initPosition(ref, store); Holder holder = store.getRegistry().newHolder(); holder.addComponent(NetworkId.getComponentType(), new NetworkId(store.getExternalData().takeNextNetworkId())); holder.addComponent(EntityStore.REGISTRY.getNonSerializedComponentType(), NonSerialized.get()); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, this.rotation)); holder.addComponent(ModelComponent.getComponentType(), new ModelComponent(model)); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(this.rotation)); this.modelPreview = store.addEntity(holder, AddReason.SPAWN); this.lastPreviewScale = this.currentScale; } } } private void updatePreviewScale(@Nonnull Ref ref, @Nonnull Store store) { if (this.modelPreview != null && this.modelPreview.isValid()) { EntityScaleComponent existingScale = store.getComponent(this.modelPreview, EntityScaleComponent.getComponentType()); if (existingScale != null) { boolean hasBlock = store.getComponent(this.modelPreview, BlockEntity.getComponentType()) != null; existingScale.setScale(hasBlock ? this.currentScale * 2.0F : this.currentScale); } else if ("Model".equals(this.activeTab) && this.selectedModelId != null) { ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset(this.selectedModelId); if (modelAsset != null) { store.putComponent( this.modelPreview, ModelComponent.getComponentType(), new ModelComponent(Model.createStaticScaledModel(modelAsset, this.currentScale)) ); } } } } private void createOrUpdateBlockPreview(@Nonnull Ref ref, @Nonnull Store store, @Nonnull String blockTypeKey) { this.clearPreview(store); this.initPosition(ref, store); Holder holder = store.getRegistry().newHolder(); holder.addComponent(NetworkId.getComponentType(), new NetworkId(store.getExternalData().takeNextNetworkId())); holder.addComponent(EntityStore.REGISTRY.getNonSerializedComponentType(), NonSerialized.get()); holder.addComponent(BlockEntity.getComponentType(), new BlockEntity(blockTypeKey)); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, this.rotation)); holder.addComponent(EntityScaleComponent.getComponentType(), new EntityScaleComponent(this.currentScale * 2.0F)); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(this.rotation)); this.modelPreview = store.addEntity(holder, AddReason.SPAWN); this.lastPreviewScale = this.currentScale; } private void createOrUpdateItemPreview(@Nonnull Ref ref, @Nonnull Store store, @Nonnull String itemId) { this.clearPreview(store); this.initPosition(ref, store); Holder holder = store.getRegistry().newHolder(); holder.addComponent(NetworkId.getComponentType(), new NetworkId(store.getExternalData().takeNextNetworkId())); holder.addComponent(EntityStore.REGISTRY.getNonSerializedComponentType(), NonSerialized.get()); ItemStack itemStack = new ItemStack(itemId, 1); itemStack.setOverrideDroppedItemAnimation(true); holder.addComponent(ItemComponent.getComponentType(), new ItemComponent(itemStack)); holder.addComponent(EntityScaleComponent.getComponentType(), new EntityScaleComponent(this.currentScale)); holder.addComponent(PreventPickup.getComponentType(), PreventPickup.INSTANCE); holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(this.position, this.rotation)); holder.addComponent(HeadRotation.getComponentType(), new HeadRotation(this.rotation)); this.modelPreview = store.addEntity(holder, AddReason.SPAWN); this.lastPreviewScale = this.currentScale; } @Nullable private Model getNPCModel() { NPCPlugin npcPlugin = NPCPlugin.get(); int roleIndex = npcPlugin.getIndex(this.selectedNpcRole); npcPlugin.forceValidation(roleIndex); BuilderInfo roleBuilderInfo = npcPlugin.getRoleBuilderInfo(roleIndex); if (roleBuilderInfo == null) { throw new IllegalStateException("Can't find a matching role builder"); } else if (!npcPlugin.testAndValidateRole(roleBuilderInfo)) { throw new GeneralCommandException(Message.translation("server.commands.npc.spawn.validation_failed")); } else { Builder roleBuilder = npcPlugin.tryGetCachedValidRole(roleIndex); if (roleBuilder == null) { throw new IllegalArgumentException("Can't find a matching role builder"); } else if (roleBuilder instanceof ISpawnableWithModel spawnable) { if (!roleBuilder.isSpawnable()) { throw new IllegalArgumentException("Abstract role templates cannot be spawned directly - a variant needs to be created!"); } else { SpawningContext spawningContext = new SpawningContext(); if (!spawningContext.setSpawnable(spawnable)) { throw new GeneralCommandException(Message.translation("server.commands.npc.spawn.cantSetRolebuilder")); } else { return spawningContext.getModel(); } } } else { throw new IllegalArgumentException("Role builder must support ISpawnableWithModel interface"); } } } public static class EntitySpawnPageEventData { static final String KEY_NPC_ROLE = "NPCRole"; static final String KEY_MODEL_ID = "ModelId"; static final String KEY_ITEM_ID = "ItemId"; static final String KEY_ITEM_STACK_ID = "ItemStackId"; static final String KEY_TYPE = "Type"; static final String KEY_TAB = "Tab"; static final String KEY_SEARCH_QUERY = "@SearchQuery"; static final String KEY_COUNT = "@Count"; static final String KEY_ROTATION_OFFSET = "@RotationOffset"; static final String KEY_SCALE = "@Scale"; public static final BuilderCodec CODEC = BuilderCodec.builder( EntitySpawnPage.EntitySpawnPageEventData.class, EntitySpawnPage.EntitySpawnPageEventData::new ) .append(new KeyedCodec<>("NPCRole", Codec.STRING), (entry, s) -> entry.npcRole = s, entry -> entry.npcRole) .add() .append(new KeyedCodec<>("ModelId", Codec.STRING), (entry, s) -> entry.modelId = s, entry -> entry.modelId) .add() .append(new KeyedCodec<>("ItemId", Codec.STRING), (entry, s) -> entry.itemId = s, entry -> entry.itemId) .add() .append(new KeyedCodec<>("ItemStackId", Codec.STRING), (entry, s) -> entry.itemStackId = s, entry -> entry.itemStackId) .add() .append(new KeyedCodec<>("Type", Codec.STRING), (entry, s) -> entry.type = s, entry -> entry.type) .add() .append(new KeyedCodec<>("Tab", Codec.STRING), (entry, s) -> entry.tab = s, entry -> entry.tab) .add() .append(new KeyedCodec<>("@SearchQuery", Codec.STRING), (entry, s) -> entry.searchQuery = s, entry -> entry.searchQuery) .add() .append(new KeyedCodec<>("@Count", Codec.INTEGER), (entry, s) -> entry.count = s, entry -> entry.count) .add() .append(new KeyedCodec<>("@RotationOffset", Codec.FLOAT), (entry, s) -> entry.rotationOffset = s, entry -> entry.rotationOffset) .add() .append(new KeyedCodec<>("@Scale", Codec.FLOAT), (entry, s) -> entry.scale = s, entry -> entry.scale) .add() .build(); private String npcRole; private String modelId; private String itemId; private String itemStackId; private String type; private String tab; private String searchQuery; private int count; private float rotationOffset; private Float scale; } }