From 1a314183f3088731baf2a10964adc0e9d9d1d1fe Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sat, 14 Feb 2026 01:55:22 -0500 Subject: [PATCH 01/12] UI changes --- HT_PBI_OVERVIEW.md | 46 +- .../github/hytech/storage/HytechStorage.java | 1 + .../storage/ui/TerminalInventoryPage.java | 753 +++++++ .../commands/ClearInventoryCommand.java | 41 + .../world/events/BlockUseEventSystem.java | 275 +-- .../UI/Custom/Pages/HytechTerminalPage.ui | 1989 +++++++++++++++++ 6 files changed, 2860 insertions(+), 245 deletions(-) create mode 100644 src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java create mode 100644 src/main/java/com/github/hytech/storage/utility/commands/ClearInventoryCommand.java create mode 100644 src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui diff --git a/HT_PBI_OVERVIEW.md b/HT_PBI_OVERVIEW.md index c48e048..291da4f 100644 --- a/HT_PBI_OVERVIEW.md +++ b/HT_PBI_OVERVIEW.md @@ -92,6 +92,47 @@ What it is: How we use it: - We open the terminal window via: `setPageWithWindows(playerRef, store, Page.Bench, true, window)` +- We now also open a custom terminal page layered with windows via: + `openCustomPageWithWindows(playerRef, store, customPage, window)` + +### InteractiveCustomUIPage +What it is: +- A custom page base class that supports typed event payloads coming from UI controls. + +How we use it: +- `TerminalInventoryPage` extends `InteractiveCustomUIPage`. +- Slot button click events are decoded through a `BuilderCodec` into `TerminalEventData`. + +### CustomPageLifetime +What it is: +- Policy enum for how a custom page behaves (dismissable, persistent, etc). + +How we use it: +- Terminal custom page uses `CustomPageLifetime.CanDismiss` so players can close it normally. + +### UICommandBuilder +What it is: +- Builder used by custom pages to send UI layout/field update commands to the client. + +How we use it: +- Terminal custom page calls `append("Pages/HytechTerminalPage.ui")` to render the page. + +### UIEventBuilder / EventData / CustomUIEventBindingType +What they are: +- `UIEventBuilder` registers UI interactions (button press, value changed, etc). +- `EventData` carries key/value payloads from UI controls. +- `CustomUIEventBindingType` defines which UI event type to bind. + +How we use them: +- Terminal page binds `Activating` on each slot button and refresh button. +- Slot index and action type are passed via `EventData` and decoded into typed event objects. + +### PlayerRef +What it is: +- A universe-level player reference object used by page/window managers. + +How we use it: +- We pass `player.getPlayerRef()` into custom page constructors. ### ContainerBlockWindow What it is: @@ -206,8 +247,9 @@ The terminal interaction flow looks like this: 2) UseBlockEvent.Pre fires. 3) BlockUseEventSystem checks that the block is a Terminal. 4) We build a SimpleItemContainer from network storage data. -5) We open a ContainerBlockWindow via PageManager. -6) When the window closes, we persist items back into storage and save. +5) We open a custom terminal page via PageManager. +6) Slot button clicks trigger typed page events. +7) The page withdraws max-sized stacks to player inventory and updates network storage. ## Where To Look In Code diff --git a/src/main/java/com/github/hytech/storage/HytechStorage.java b/src/main/java/com/github/hytech/storage/HytechStorage.java index a9ef7da..1678fea 100644 --- a/src/main/java/com/github/hytech/storage/HytechStorage.java +++ b/src/main/java/com/github/hytech/storage/HytechStorage.java @@ -46,6 +46,7 @@ protected void setup() { } private void registerCommands() { + this.getCommandRegistry().registerCommand(new ClearInventoryCommand()); this.getCommandRegistry().registerCommand(new ListNetworksCommand()); this.getCommandRegistry().registerCommand(new ListServerRacksCommand()); this.getCommandRegistry().registerCommand(new ListServerStoragesCommand()); diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java new file mode 100644 index 0000000..2566f27 --- /dev/null +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -0,0 +1,753 @@ +package com.github.hytech.storage.ui; + +import com.github.hytech.storage.network.Network; +import com.github.hytech.storage.network.SNetworkManager; +import com.github.hytech.storage.network.device.DeviceServerStorage; +import com.github.hytech.storage.network.device.DeviceTerminal; +import com.github.hytech.storage.state.StateManager; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +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.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.inventory.container.SimpleItemContainer; +import com.hypixel.hytale.server.core.ui.ItemGridSlot; +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.storage.EntityStore; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Interactive custom terminal page used to withdraw items without vanilla drag behavior. + */ +public class TerminalInventoryPage extends InteractiveCustomUIPage { + private static final String LAYOUT_PATH = "Pages/HytechTerminalPage.ui"; + private static final int SLOT_BUTTON_COUNT = 40; + private static final int PLAYER_STORAGE_VISIBLE_SLOTS = 30; + private static final int PLAYER_HOTBAR_VISIBLE_SLOTS = 10; + private static final String ACTION_TAKE = "Take"; + private static final String ACTION_REFRESH = "Refresh"; + private static final String ACTION_DEPOSIT_HELD = "DepositHeld"; + private static final String ACTION_DEPOSIT_HOTBAR = "DepositHotbar"; + private static final String ACTION_DEPOSIT_INVENTORY = "DepositInventory"; + private static final String ACTION_DEPOSIT_PLAYER_SLOT = "DepositPlayerSlot"; + private static final String ACTION_SORT_NAME = "SortName"; + private static final String ACTION_SORT_COUNT = "SortCount"; + + private final Vector3i terminalPosition; + private String searchQuery; + private SortField sortField; + private boolean sortDescending; + + /** + * Creates the terminal custom page. + * + * @param playerRef owning player reference + * @param terminalPosition world position for the terminal being used + */ + public TerminalInventoryPage(@Nonnull PlayerRef playerRef, @Nonnull Vector3i terminalPosition) { + super(playerRef, CustomPageLifetime.CanDismiss, TerminalEventData.CODEC); + this.terminalPosition = terminalPosition; + this.searchQuery = ""; + this.sortField = SortField.NAME; + this.sortDescending = false; + } + + /** + * Builds the initial page layout and binds click events. + */ + @Override + public void build( + @NonNullDecl Ref ref, + UICommandBuilder commandBuilder, + @NonNullDecl UIEventBuilder eventBuilder, + @NonNullDecl Store store + ) { + commandBuilder.append(LAYOUT_PATH); + bindEvents(eventBuilder); + updateSlotLabels(commandBuilder, resolveSlots(resolveNetwork()), "Select a slot to withdraw max stack."); + updatePlayerInventoryGrids(commandBuilder, ref, store); + commandBuilder.set("#SearchInput.Value", searchQuery); + updateControlLabels(commandBuilder); + } + + /** + * Handles data events emitted by UI controls. + */ + @Override + public void handleDataEvent( + @NonNullDecl Ref playerRef, + @NonNullDecl Store store, + @NonNullDecl TerminalEventData data + ) { + if (data.searchQuery != null) { + searchQuery = data.searchQuery.trim().toLowerCase(); + String message = searchQuery.isEmpty() + ? "Search cleared." + : "Search: " + searchQuery; + refresh(playerRef, store, message); + if (isNullOrBlank(data.type)) { + return; + } + } + + if (isNullOrBlank(data.type)) { + return; + } + + if (ACTION_REFRESH.equalsIgnoreCase(data.type)) { + refresh(playerRef, store, "Refreshed."); + return; + } + + if (ACTION_DEPOSIT_HELD.equalsIgnoreCase(data.type)) { + refresh(playerRef, store, depositHeld(playerRef, store)); + return; + } + + if (ACTION_DEPOSIT_HOTBAR.equalsIgnoreCase(data.type)) { + refresh(playerRef, store, depositFromContainer(playerRef, store, true, false)); + return; + } + + if (ACTION_DEPOSIT_INVENTORY.equalsIgnoreCase(data.type)) { + refresh(playerRef, store, depositFromContainer(playerRef, store, false, true)); + return; + } + + if (ACTION_DEPOSIT_PLAYER_SLOT.equalsIgnoreCase(data.type) && data.slot != null) { + refresh(playerRef, store, depositSinglePlayerSlot(playerRef, store, data.slot)); + return; + } + + if (ACTION_SORT_NAME.equalsIgnoreCase(data.type)) { + applySortSelection(SortField.NAME); + refresh(playerRef, store, "Sort: Name " + (sortDescending ? "descending" : "ascending") + "."); + return; + } + + if (ACTION_SORT_COUNT.equalsIgnoreCase(data.type)) { + applySortSelection(SortField.COUNT); + refresh(playerRef, store, "Sort: Count " + (sortDescending ? "descending" : "ascending") + "."); + return; + } + + if (ACTION_TAKE.equalsIgnoreCase(data.type) && data.slot != null) { + int slotIndex = parseSlotIndex(data.slot); + String status = slotIndex >= 0 + ? withdrawFromSlot(playerRef, store, slotIndex) + : "Invalid slot selection."; + refresh(playerRef, store, status); + return; + } + + refresh(playerRef, store, "Unhandled action: " + data.type); + } + + /** + * Registers button events for slot actions and refresh. + */ + private void bindEvents(UIEventBuilder eventBuilder) { + for (int slot = 0; slot < SLOT_BUTTON_COUNT; slot++) { + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#Slot" + slot, + EventData.of("Type", ACTION_TAKE).append("Slot", String.valueOf(slot)), + false + ); + } + for (int slot = 0; slot < PLAYER_STORAGE_VISIBLE_SLOTS; slot++) { + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#PlayerStorageSlot" + slot, + EventData.of("Type", ACTION_DEPOSIT_PLAYER_SLOT).append("Slot", "S" + slot), + false + ); + } + for (int slot = 0; slot < PLAYER_HOTBAR_VISIBLE_SLOTS; slot++) { + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#PlayerHotbarSlot" + slot, + EventData.of("Type", ACTION_DEPOSIT_PLAYER_SLOT).append("Slot", "H" + slot), + false + ); + } + + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#Refresh", + EventData.of("Type", ACTION_REFRESH), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#DepositHeld", + EventData.of("Type", ACTION_DEPOSIT_HELD), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#DepositHotbar", + EventData.of("Type", ACTION_DEPOSIT_HOTBAR), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#DepositInventory", + EventData.of("Type", ACTION_DEPOSIT_INVENTORY), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#SortByName", + EventData.of("Type", ACTION_SORT_NAME), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#SortByCount", + EventData.of("Type", ACTION_SORT_COUNT), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.ValueChanged, + "#SearchInput", + EventData.of("@SearchQuery", "#SearchInput.Value"), + false + ); + } + + /** + * Parses slot text payload into an integer slot index. + */ + private int parseSlotIndex(String slot) { + try { + return Integer.parseInt(slot); + } catch (NumberFormatException ignored) { + return -1; + } + } + + /** + * Checks whether a string is null or only whitespace. + */ + private boolean isNullOrBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + /** + * Refreshes slot text and status label after an action. + */ + private void refresh(Ref playerRef, Store store, String status) { + UICommandBuilder commands = new UICommandBuilder(); + updateSlotLabels(commands, resolveSlots(resolveNetwork()), status); + updatePlayerInventoryGrids(commands, playerRef, store); + updateControlLabels(commands); + sendUpdate(commands, null, false); + } + + /** + * Updates dynamic control labels. + */ + private void updateControlLabels(UICommandBuilder commands) { + String nameOrder = sortField == SortField.NAME ? (sortDescending ? " (desc)" : " (asc)") : ""; + String countOrder = sortField == SortField.COUNT ? (sortDescending ? " (desc)" : " (asc)") : ""; + commands.set("#SortByName.Text", "Name" + nameOrder); + commands.set("#SortByCount.Text", "Count" + countOrder); + } + + /** + * Applies selected sort field; repeated selection reverses direction. + */ + private void applySortSelection(SortField selectedField) { + if (sortField == selectedField) { + sortDescending = !sortDescending; + return; + } + sortField = selectedField; + sortDescending = selectedField == SortField.COUNT; + } + + /** + * Withdraws one max-size stack from the selected slot into player inventory. + */ + private String withdrawFromSlot(Ref playerRef, Store store, int slotIndex) { + Network network = resolveNetwork(); + if (network == null) { + return "Terminal is not connected to a network."; + } + + List slots = resolveSlots(network); + if (slotIndex < 0 || slotIndex >= slots.size()) { + return "Slot is empty."; + } + + ItemView selected = slots.get(slotIndex); + int maxStack = resolveMaxStackSize(selected.itemId); + int amountToWithdraw = Math.min(selected.count, maxStack); + if (amountToWithdraw <= 0) { + return "Nothing to withdraw."; + } + + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + return "Player unavailable."; + } + + SimpleItemContainer.addOrDropItemStack( + store, + playerRef, + player.getInventory().getCombinedEverything(), + new ItemStack(selected.itemId, amountToWithdraw) + ); + + removeFromNetworkStorage(network, selected.itemId, amountToWithdraw); + StateManager.getInstance().save(); + return "Withdrew " + amountToWithdraw + " of " + selected.itemId + "."; + } + + /** + * Deposits the currently held hotbar stack into network storage. + */ + private String depositHeld(Ref playerRef, Store store) { + Network network = resolveNetwork(); + if (network == null) { + return "Terminal is not connected to a network."; + } + + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + return "Player unavailable."; + } + + Inventory inventory = player.getInventory(); + ItemContainer hotbar = inventory.getHotbar(); + short slot = (short) inventory.getActiveHotbarSlot(); + if (slot < 0 || slot >= hotbar.getCapacity()) { + return "No held item."; + } + + ItemStack held = hotbar.getItemStack(slot); + if (held == null || held.isEmpty()) { + return "No held item."; + } + + int accepted = addIntoNetworkStorage(network, held.getItemId(), held.getQuantity()); + if (accepted <= 0) { + return "Network is full."; + } + + int remaining = held.getQuantity() - accepted; + if (remaining > 0) { + hotbar.setItemStackForSlot(slot, new ItemStack(held.getItemId(), remaining)); + } else { + hotbar.setItemStackForSlot(slot, ItemStack.EMPTY); + } + + StateManager.getInstance().save(); + return "Deposited " + accepted + " of " + held.getItemId() + "."; + } + + /** + * Deposits all stacks from selected inventory sections into network storage. + */ + private String depositFromContainer( + Ref playerRef, + Store store, + boolean includeHotbar, + boolean includeStorage + ) { + Network network = resolveNetwork(); + if (network == null) { + return "Terminal is not connected to a network."; + } + + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + return "Player unavailable."; + } + + Inventory inventory = player.getInventory(); + int moved = 0; + + if (includeHotbar) { + moved += depositFromItemContainer(network, inventory.getHotbar()); + } + if (includeStorage) { + moved += depositFromItemContainer(network, inventory.getStorage()); + } + + if (moved <= 0) { + return "Nothing deposited."; + } + + StateManager.getInstance().save(); + return "Deposited " + moved + " items."; + } + + /** + * Deposits items from one container and mutates source slots with leftovers. + */ + private int depositFromItemContainer(Network network, ItemContainer source) { + int moved = 0; + for (short slot = 0; slot < source.getCapacity(); slot++) { + ItemStack stack = source.getItemStack(slot); + if (stack == null || stack.isEmpty()) { + continue; + } + + int accepted = addIntoNetworkStorage(network, stack.getItemId(), stack.getQuantity()); + if (accepted <= 0) { + continue; + } + + moved += accepted; + int remaining = stack.getQuantity() - accepted; + if (remaining > 0) { + source.setItemStackForSlot(slot, new ItemStack(stack.getItemId(), remaining)); + } else { + source.setItemStackForSlot(slot, ItemStack.EMPTY); + } + } + return moved; + } + + /** + * Deposits one specific player slot stack into network storage. + */ + private String depositSinglePlayerSlot( + Ref playerRef, + Store store, + String encodedSlot + ) { + if (encodedSlot == null || encodedSlot.length() < 2) { + return "Invalid player slot."; + } + + Network network = resolveNetwork(); + if (network == null) { + return "Terminal is not connected to a network."; + } + + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + return "Player unavailable."; + } + + char section = Character.toUpperCase(encodedSlot.charAt(0)); + int slot; + try { + slot = Integer.parseInt(encodedSlot.substring(1)); + } catch (NumberFormatException ignored) { + return "Invalid player slot."; + } + + Inventory inventory = player.getInventory(); + ItemContainer source; + if (section == 'S') { + source = inventory.getStorage(); + } else if (section == 'H') { + source = inventory.getHotbar(); + } else { + return "Invalid player slot."; + } + + if (slot < 0 || slot >= source.getCapacity()) { + return "Invalid player slot."; + } + + ItemStack stack = source.getItemStack((short) slot); + if (stack == null || stack.isEmpty()) { + return "Slot is empty."; + } + + int accepted = addIntoNetworkStorage(network, stack.getItemId(), stack.getQuantity()); + if (accepted <= 0) { + return "Network is full."; + } + + int remaining = stack.getQuantity() - accepted; + if (remaining > 0) { + source.setItemStackForSlot((short) slot, new ItemStack(stack.getItemId(), remaining)); + } else { + source.setItemStackForSlot((short) slot, ItemStack.EMPTY); + } + + StateManager.getInstance().save(); + return "Deposited " + accepted + " of " + stack.getItemId() + "."; + } + + /** + * Adds quantity to network totals, respecting aggregate capacity. + */ + private int addIntoNetworkStorage(Network network, String itemId, int requested) { + if (requested <= 0) { + return 0; + } + + Map totals = getNetworkTotals(network); + int used = totals.values().stream().mapToInt(Integer::intValue).sum(); + int max = network.getServerStorages().size() * DeviceServerStorage.getStorageMax(); + int free = Math.max(0, max - used); + int accepted = Math.min(requested, free); + if (accepted <= 0) { + return 0; + } + + totals.merge(itemId, accepted, Integer::sum); + writeNetworkTotals(network, totals); + return accepted; + } + + /** + * Removes a quantity of one item id from storage devices in insertion order. + */ + private void removeFromNetworkStorage(Network network, String itemId, int amount) { + int remaining = amount; + for (DeviceServerStorage storage : network.getServerStorages().values()) { + if (remaining <= 0) { + break; + } + Map items = storage.getItems(); + int current = items.getOrDefault(itemId, 0); + if (current <= 0) { + continue; + } + int take = Math.min(current, remaining); + int next = current - take; + if (next <= 0) { + items.remove(itemId); + } else { + items.put(itemId, next); + } + remaining -= take; + } + } + + /** + * Resolves merged network item totals and maps the first rows to page slots. + */ + private List resolveSlots(Network network) { + List visible = new ArrayList<>(); + if (network == null) { + return visible; + } + + List all = getFilteredItems(network); + int end = Math.min(all.size(), SLOT_BUTTON_COUNT); + for (int i = 0; i < end; i++) { + visible.add(all.get(i)); + } + return visible; + } + + /** + * Returns filtered items using the current search query. + */ + private List getFilteredItems(Network network) { + List filtered = new ArrayList<>(); + Map totals = getNetworkTotals(network); + for (Map.Entry entry : totals.entrySet()) { + String id = entry.getKey(); + if (!searchQuery.isEmpty() && !id.toLowerCase().contains(searchQuery)) { + continue; + } + filtered.add(new ItemView(id, entry.getValue())); + } + Comparator comparator = sortField == SortField.COUNT + ? Comparator.comparingInt((ItemView item) -> item.count).thenComparing(item -> item.itemId.toLowerCase()) + : Comparator.comparing((ItemView item) -> item.itemId.toLowerCase()).thenComparingInt(item -> item.count); + if (sortDescending) { + comparator = comparator.reversed(); + } + filtered.sort(comparator); + return filtered; + } + + /** + * Reads merged totals from all storage blocks in this network. + */ + private Map getNetworkTotals(Network network) { + Map totals = new LinkedHashMap<>(); + for (DeviceServerStorage storage : network.getServerStorages().values()) { + for (Map.Entry entry : storage.getItems().entrySet()) { + totals.merge(entry.getKey(), entry.getValue(), Integer::sum); + } + } + return totals; + } + + /** + * Writes merged totals back into storage blocks in first-added order. + */ + private void writeNetworkTotals(Network network, Map totals) { + Map remaining = new LinkedHashMap<>(totals); + for (DeviceServerStorage storage : network.getServerStorages().values()) { + Map next = new LinkedHashMap<>(); + int used = 0; + Iterator> iterator = remaining.entrySet().iterator(); + while (iterator.hasNext() && used < DeviceServerStorage.getStorageMax()) { + Map.Entry entry = iterator.next(); + int available = DeviceServerStorage.getStorageMax() - used; + int take = Math.min(entry.getValue(), available); + if (take > 0) { + next.put(entry.getKey(), take); + used += take; + int left = entry.getValue() - take; + if (left <= 0) { + iterator.remove(); + } else { + entry.setValue(left); + } + } + } + storage.setItems(next); + } + } + + /** + * Writes display text into slot buttons and status label. + */ + private void updateSlotLabels(UICommandBuilder commands, List slots, String status) { + for (int i = 0; i < SLOT_BUTTON_COUNT; i++) { + ItemGridSlot[] slotData = i < slots.size() + ? new ItemGridSlot[]{new ItemGridSlot(new ItemStack(slots.get(i).itemId, slots.get(i).count))} + : new ItemGridSlot[]{new ItemGridSlot()}; + commands.set("#SlotGrid" + i + ".Slots", slotData); + } + commands.set("#Status.Text", status); + } + + /** + * Mirrors the player's storage and hotbar inventories into the custom page. + */ + private void updatePlayerInventoryGrids( + UICommandBuilder commands, + Ref playerRef, + Store store + ) { + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + for (int i = 0; i < PLAYER_STORAGE_VISIBLE_SLOTS; i++) { + commands.set("#PlayerStorageGrid" + i + ".Slots", new ItemGridSlot[]{new ItemGridSlot()}); + } + for (int i = 0; i < PLAYER_HOTBAR_VISIBLE_SLOTS; i++) { + commands.set("#PlayerHotbarGrid" + i + ".Slots", new ItemGridSlot[]{new ItemGridSlot()}); + } + return; + } + + Inventory inventory = player.getInventory(); + ItemContainer storage = inventory.getStorage(); + ItemContainer hotbar = inventory.getHotbar(); + + for (int i = 0; i < PLAYER_STORAGE_VISIBLE_SLOTS; i++) { + ItemStack stack = i < storage.getCapacity() ? storage.getItemStack((short) i) : null; + ItemGridSlot[] data = (stack == null || stack.isEmpty()) + ? new ItemGridSlot[]{new ItemGridSlot()} + : new ItemGridSlot[]{new ItemGridSlot(stack)}; + commands.set("#PlayerStorageGrid" + i + ".Slots", data); + } + for (int i = 0; i < PLAYER_HOTBAR_VISIBLE_SLOTS; i++) { + ItemStack stack = i < hotbar.getCapacity() ? hotbar.getItemStack((short) i) : null; + ItemGridSlot[] data = (stack == null || stack.isEmpty()) + ? new ItemGridSlot[]{new ItemGridSlot()} + : new ItemGridSlot[]{new ItemGridSlot(stack)}; + commands.set("#PlayerHotbarGrid" + i + ".Slots", data); + } + } + + /** + * Resolves the network connected to this terminal page's terminal position. + */ + private Network resolveNetwork() { + DeviceTerminal terminal = SNetworkManager.getInstance().getTerminals().get(terminalPosition); + return terminal != null ? terminal.getNetwork() : null; + } + + /** + * Resolves runtime max stack size for an item id. + */ + private int resolveMaxStackSize(String itemId) { + ItemStack probe = new ItemStack(itemId, 1); + probe.getItem(); + return Math.max(1, probe.getItem().getMaxStack()); + } + + /** + * Lightweight display model for slot rows. + */ + private static final class ItemView { + private final String itemId; + private final int count; + + private ItemView(String itemId, int count) { + this.itemId = itemId; + this.count = count; + } + } + + private enum SortField { + NAME, + COUNT + } + + /** + * Typed UI event payload decoded from custom page events. + */ + public static final class TerminalEventData { + private static final String KEY_TYPE = "Type"; + private static final String KEY_SLOT = "Slot"; + private static final String KEY_SEARCH_QUERY = "@SearchQuery"; + + public static final BuilderCodec CODEC = + BuilderCodec.builder(TerminalEventData.class, TerminalEventData::new) + .append( + new KeyedCodec<>(KEY_TYPE, Codec.STRING), + (data, value) -> data.type = value, + data -> data.type + ).add() + .append( + new KeyedCodec<>(KEY_SLOT, Codec.STRING), + (data, value) -> data.slot = value, + data -> data.slot + ).add() + .append( + new KeyedCodec<>(KEY_SEARCH_QUERY, Codec.STRING), + (data, value) -> data.searchQuery = value, + data -> data.searchQuery + ).add() + .build(); + + private String type; + private String slot; + private String searchQuery; + + /** + * Creates empty event data for codec decoding. + */ + public TerminalEventData() { + this.type = null; + this.slot = null; + this.searchQuery = null; + } + } +} diff --git a/src/main/java/com/github/hytech/storage/utility/commands/ClearInventoryCommand.java b/src/main/java/com/github/hytech/storage/utility/commands/ClearInventoryCommand.java new file mode 100644 index 0000000..7bf9442 --- /dev/null +++ b/src/main/java/com/github/hytech/storage/utility/commands/ClearInventoryCommand.java @@ -0,0 +1,41 @@ +package com.github.hytech.storage.utility.commands; + +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; + +import javax.annotation.Nonnull; + +/** + * Command that clears the executing player's full inventory. + */ +public class ClearInventoryCommand extends CommandBase { + /** + * Creates the command used for inventory cleanup during testing. + */ + public ClearInventoryCommand() { + super("clearInventory", "Clears your full inventory, including hotbar."); + } + + /** + * Executes the command synchronously. + * + * @param ctx command context + */ + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + if (!ctx.isPlayer()) { + ctx.sendMessage(Message.raw("This command can only be run by a player.")); + return; + } + + Player player = ctx.senderAs(Player.class); + Inventory inventory = player.getInventory(); + inventory.clear(); + inventory.markChanged(); + + ctx.sendMessage(Message.raw("Inventory cleared.")); + } +} diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java index af8e32e..45b6207 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java @@ -1,10 +1,6 @@ package com.github.hytech.storage.world.events; -import com.github.hytech.storage.network.Network; -import com.github.hytech.storage.network.SNetworkManager; -import com.github.hytech.storage.network.device.DeviceServerStorage; -import com.github.hytech.storage.network.device.DeviceTerminal; -import com.github.hytech.storage.state.StateManager; +import com.github.hytech.storage.ui.TerminalInventoryPage; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Ref; @@ -16,36 +12,28 @@ import com.hypixel.hytale.server.core.entity.InteractionContext; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.entity.entities.player.pages.PageManager; -import com.hypixel.hytale.server.core.entity.entities.player.windows.ContainerBlockWindow; +import com.hypixel.hytale.server.core.entity.entities.player.windows.ContainerWindow; import com.hypixel.hytale.server.core.event.events.ecs.UseBlockEvent; -import com.hypixel.hytale.server.core.inventory.ItemStack; -import com.hypixel.hytale.server.core.inventory.container.ItemContainer; import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import com.hypixel.hytale.protocol.packets.interface_.Page; import org.checkerframework.checker.nullness.compatqual.NonNullDecl; import org.checkerframework.checker.nullness.compatqual.NullableDecl; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - /** - * ECS system that opens terminal inventory on Use interactions. + * Opens the custom terminal page when players use terminal blocks. */ public class BlockUseEventSystem extends EntityEventSystem { private static final String TERMINAL_BLOCK_ID = "Terminal"; - private static final short TERMINAL_CONTAINER_CAPACITY = 63; /** - * Builds the system for terminal use interactions. + * Registers the UseBlock pre-event listener. */ public BlockUseEventSystem() { super(UseBlockEvent.Pre.class); } /** - * Handles Use interactions on blocks and opens the terminal window when appropriate. + * Opens the terminal custom page for terminal block interactions. */ @Override public void handle( @@ -55,262 +43,63 @@ public void handle( @NonNullDecl CommandBuffer commandBuffer, @NonNullDecl UseBlockEvent.Pre event ) { - BlockType blockType = event.getBlockType(); - if (!isTerminalBlock(blockType)) { + if (!isTerminalBlock(event.getBlockType())) { return; } Ref playerRef = resolvePlayerRef(event); - Player player = resolvePlayer(commandBuffer, playerRef); + if (playerRef == null) { + return; + } + + Player player = store.getComponent(playerRef, Player.getComponentType()); if (player == null) { return; } - Network network = resolveNetwork(event.getTargetBlock()); - ItemContainer container = buildTerminalContainer(network); - registerCapacityEnforcer(container, playerRef, store, network); - ContainerBlockWindow window = buildTerminalWindow(event, blockType, container); - registerPersistOnClose(window, container, playerRef, store, network); - boolean opened = openTerminalWindow(player, playerRef, store, window); + Vector3i terminalPosition = event.getTargetBlock(); + TerminalInventoryPage page = new TerminalInventoryPage(player.getPlayerRef(), terminalPosition); + PageManager pageManager = player.getPageManager(); + boolean opened = pageManager.openCustomPageWithWindows( + playerRef, + store, + page, + new ContainerWindow(new SimpleItemContainer((short) 1)) + ); if (!opened) { - return; + pageManager.openCustomPage(playerRef, store, page); } event.setCancelled(true); } /** - * Resolves the player entity reference from the interaction context. + * Resolves the interacting entity reference from interaction context. */ private Ref resolvePlayerRef(UseBlockEvent.Pre event) { InteractionContext context = event.getContext(); - return context.getEntity(); - } - - /** - * Retrieves the Player component from the command buffer. - */ - private Player resolvePlayer(CommandBuffer commandBuffer, Ref playerRef) { - return commandBuffer.getComponent(playerRef, Player.getComponentType()); - } - - /** - * Builds a window for the terminal inventory view. - */ - private ContainerBlockWindow buildTerminalWindow( - UseBlockEvent.Pre event, - BlockType blockType, - ItemContainer container - ) { - Vector3i pos = event.getTargetBlock(); - int rotationIndex = 0; - return new ContainerBlockWindow( - pos.x, - pos.y, - pos.z, - rotationIndex, - blockType, - container - ); + return context != null ? context.getEntity() : null; } /** - * Opens the terminal window on the standard Bench page. + * Checks whether the used block is the terminal block. */ - private boolean openTerminalWindow( - Player player, - Ref playerRef, - Store store, - ContainerBlockWindow window - ) { - PageManager pageManager = player.getPageManager(); - return pageManager.setPageWithWindows(playerRef, store, Page.Bench, true, window); - } - - /** - * Registers immediate capacity enforcement on container changes. - */ - private void registerCapacityEnforcer( - ItemContainer container, - Ref playerRef, - Store store, - Network network - ) { - final boolean[] enforcing = {false}; - container.registerChangeEvent(changeEvent -> { - if (enforcing[0]) { - return; - } - enforcing[0] = true; - enforceCapacity(container, playerRef, store, network); - enforcing[0] = false; - }); - } - - /** - * Persists terminal contents on window close. - */ - private void registerPersistOnClose( - ContainerBlockWindow window, - ItemContainer container, - Ref playerRef, - Store store, - Network network - ) { - window.registerCloseEvent(closeEvent -> { - enforceCapacity(container, playerRef, store, network); - persistContainerToStorage(container, network); - StateManager.getInstance().save(); - }); - } - - /** - * Checks whether the target block is the terminal. - */ - private boolean isTerminalBlock(@NonNullDecl BlockType blockType) { + private boolean isTerminalBlock(BlockType blockType) { + if (blockType == null) { + return false; + } String blockId = blockType.getId(); if (blockId == null) { return false; } - int separatorIndex = blockId.indexOf(':'); - if (separatorIndex >= 0) { - blockId = blockId.substring(separatorIndex + 1); + int separator = blockId.indexOf(':'); + if (separator >= 0) { + blockId = blockId.substring(separator + 1); } return TERMINAL_BLOCK_ID.equalsIgnoreCase(blockId); } /** - * Builds a temporary container populated from network storage. - */ - private ItemContainer buildTerminalContainer(Network network) { - SimpleItemContainer container = new SimpleItemContainer(TERMINAL_CONTAINER_CAPACITY); - short slot = 0; - Map storages = - network != null ? network.getServerStorages() : SNetworkManager.getInstance().getServerStorages(); - for (DeviceServerStorage storage : storages.values()) { - for (Map.Entry entry : storage.getItems().entrySet()) { - if (slot >= TERMINAL_CONTAINER_CAPACITY) { - return container; - } - container.setItemStackForSlot(slot, new ItemStack(entry.getKey(), entry.getValue())); - slot++; - } - } - return container; - } - - /** - * Writes the container contents into storage blocks. - */ - private void persistContainerToStorage(ItemContainer container, Network network) { - Map storages = - network != null ? network.getServerStorages() : SNetworkManager.getInstance().getServerStorages(); - if (storages.isEmpty()) { - return; - } - Map items = new LinkedHashMap<>(); - for (short slot = 0; slot < TERMINAL_CONTAINER_CAPACITY; slot++) { - ItemStack stack = container.getItemStack(slot); - if (stack == null || stack.isEmpty()) { - continue; - } - items.merge(stack.getItemId(), stack.getQuantity(), Integer::sum); - } - distributeItemsAcrossStorages(items, storages); - } - - /** - * Distributes items across storage blocks in first-added order. - */ - private void distributeItemsAcrossStorages( - Map items, - Map storages - ) { - Map remaining = new LinkedHashMap<>(items); - for (DeviceServerStorage storage : storages.values()) { - Map storageItems = new HashMap<>(); - int used = 0; - var iterator = remaining.entrySet().iterator(); - while (iterator.hasNext() && used < DeviceServerStorage.getStorageMax()) { - Map.Entry entry = iterator.next(); - int available = DeviceServerStorage.getStorageMax() - used; - int take = Math.min(entry.getValue(), available); - if (take > 0) { - storageItems.put(entry.getKey(), take); - used += take; - int left = entry.getValue() - take; - if (left <= 0) { - iterator.remove(); - } else { - entry.setValue(left); - } - } - } - storage.setItems(storageItems); - } - } - - /** - * Enforces total capacity and returns overflow to the player. - */ - private void enforceCapacity( - ItemContainer container, - Ref playerRef, - Store store, - Network network - ) { - Map storages = - network != null ? network.getServerStorages() : SNetworkManager.getInstance().getServerStorages(); - int maxTotal = storages.size() * DeviceServerStorage.getStorageMax(); - int total = 0; - for (short slot = 0; slot < TERMINAL_CONTAINER_CAPACITY; slot++) { - ItemStack stack = container.getItemStack(slot); - if (stack != null && !stack.isEmpty()) { - total += stack.getQuantity(); - } - } - if (total <= maxTotal) { - return; - } - int overflow = total - maxTotal; - Player player = store.getComponent(playerRef, Player.getComponentType()); - for (short slot = (short) (TERMINAL_CONTAINER_CAPACITY - 1); slot >= 0 && overflow > 0; slot--) { - ItemStack stack = container.getItemStack(slot); - if (stack == null || stack.isEmpty()) { - continue; - } - int qty = stack.getQuantity(); - int remove = Math.min(qty, overflow); - int newQty = qty - remove; - if (newQty > 0) { - container.setItemStackForSlot(slot, new ItemStack(stack.getItemId(), newQty)); - } else { - container.setItemStackForSlot(slot, ItemStack.EMPTY); - } - if (player != null && remove > 0) { - SimpleItemContainer.addOrDropItemStack( - store, - playerRef, - player.getInventory().getCombinedEverything(), - new ItemStack(stack.getItemId(), remove) - ); - } - overflow -= remove; - } - } - - /** - * Resolves the terminal's network from its block position. - */ - private Network resolveNetwork(Vector3i position) { - DeviceTerminal terminal = SNetworkManager.getInstance().getTerminals().get(position); - if (terminal != null) { - return terminal.getNetwork(); - } - return null; - } - - /** - * ECS query for this system. + * Query scope for this event system. */ @NullableDecl @Override diff --git a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui new file mode 100644 index 0000000..f480b5c --- /dev/null +++ b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui @@ -0,0 +1,1989 @@ +$C = "../Common.ui"; + +@SlotSize = 46; +@SlotGap = 6; +@GridWidth = 514; + +Group #TerminalRoot { + Anchor: (Full: 0); + + Group { + Anchor: (Full: 0); + LayoutMode: Center; + + $C.@Container #TerminalWindow { + Anchor: (Width: 820, Height: 780); + + #Title { + Group { + LayoutMode: Top; + Anchor: (Top: 6); + Label { + Text: "HYTECH TERMINAL"; + Style: (FontSize: 28, RenderUppercase: true, RenderBold: true, HorizontalAlignment: Center, VerticalAlignment: Center, TextColor: #d0d9e4); + } + Label { + Anchor: (Top: 6, Height: 22); + Text: "Terminal storage above, player inventory below."; + Style: (FontSize: 14, HorizontalAlignment: Center, VerticalAlignment: Center, TextColor: #8fa2b5); + } + } + } + + #Content { + Group { + LayoutMode: Top; + Anchor: (Top: 8, Bottom: 10); + + Group { + Anchor: (Height: 56); + LayoutMode: Top; + Label { + Anchor: (Height: 18); + Text: "Search Item Id"; + Style: (...$C.@DefaultLabelStyle, FontSize: 13, RenderUppercase: true); + } + Group { + Anchor: (Height: 34, Top: 4); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + TextField #SearchInput { + Anchor: (Horizontal: 8, Vertical: 5); + Value: ""; + } + } + } + + Label { + Anchor: (Top: 10, Height: 20); + Text: "Terminal Network"; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); + } + + Group { + Anchor: (Top: 6, Height: 232); + LayoutMode: Center; + Group { + Anchor: (Width: @GridWidth, Height: 232); + LayoutMode: Top; + Group { + Anchor: (Height: @SlotSize, Top: 0); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #SlotGrid0 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot0 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid1 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot1 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid2 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot2 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid3 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot3 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid4 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot4 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid5 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot5 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid6 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot6 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid7 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot7 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid8 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot8 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid9 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot9 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + Group { + Anchor: (Height: @SlotSize, Top: 6); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #SlotGrid10 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot10 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid11 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot11 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid12 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot12 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid13 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot13 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid14 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot14 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid15 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot15 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid16 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot16 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid17 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot17 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid18 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot18 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid19 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot19 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + Group { + Anchor: (Height: @SlotSize, Top: 6); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #SlotGrid20 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot20 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid21 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot21 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid22 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot22 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid23 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot23 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid24 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot24 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid25 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot25 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid26 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot26 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid27 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot27 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid28 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot28 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid29 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot29 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + Group { + Anchor: (Height: @SlotSize, Top: 6); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #SlotGrid30 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot30 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid31 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot31 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid32 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot32 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid33 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot33 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid34 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot34 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid35 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot35 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid36 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot36 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid37 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot37 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid38 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot38 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #SlotGrid39 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #Slot39 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + } + } + + Group { + Anchor: (Height: 32, Top: 8); + LayoutMode: Left; + $C.@SmallSecondaryTextButton #DepositHeld { Anchor: (Width: 170); Text: "Deposit Held"; } + $C.@SmallSecondaryTextButton #DepositHotbar { Anchor: (Width: 170, Left: 8); Text: "Deposit Hotbar"; } + $C.@SmallSecondaryTextButton #DepositInventory { Anchor: (Width: 170, Left: 8); Text: "Deposit Inventory"; } + $C.@SmallSecondaryTextButton #Refresh { Anchor: (Width: 100, Left: 8); Text: "Refresh"; } + $C.@SmallSecondaryTextButton #SortByName { Anchor: (Width: 120, Left: 8); Text: "Name (asc)"; } + $C.@SmallSecondaryTextButton #SortByCount { Anchor: (Width: 130, Left: 8); Text: "Count"; } + } + + Label { + Anchor: (Top: 12, Height: 20); + Text: "Player Inventory (Click slot to deposit)"; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); + } + + Group { + Anchor: (Top: 6, Height: 156); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + LayoutMode: Center; + Group { + Anchor: (Width: @GridWidth, Height: 138); + LayoutMode: Top; + Group { + Anchor: (Height: @SlotSize, Top: 0); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid0 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot0 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid1 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot1 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid2 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot2 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid3 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot3 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid4 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot4 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid5 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot5 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid6 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot6 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid7 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot7 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid8 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot8 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid9 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot9 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + Group { + Anchor: (Height: @SlotSize, Top: 6); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid10 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot10 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid11 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot11 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid12 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot12 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid13 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot13 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid14 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot14 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid15 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot15 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid16 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot16 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid17 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot17 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid18 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot18 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid19 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot19 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + Group { + Anchor: (Height: @SlotSize, Top: 6); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid20 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot20 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid21 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot21 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid22 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot22 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid23 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot23 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid24 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot24 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid25 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot25 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid26 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot26 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid27 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot27 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid28 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot28 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerStorageGrid29 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot29 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + } + } + + Group { + Anchor: (Top: 8, Height: 62); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + LayoutMode: Center; + Group { + Anchor: (Width: @GridWidth, Height: 46); + LayoutMode: Left; + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid0 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot0 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid1 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot1 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid2 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot2 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid3 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot3 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid4 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot4 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid5 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot5 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid6 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot6 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid7 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot7 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid8 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot8 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + Group { + Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + LayoutMode: Full; + ItemGrid #PlayerHotbarGrid9 { + Anchor: (Width: @SlotSize, Height: @SlotSize); + SlotsPerRow: 1; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: 0, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerHotbarSlot9 { + Anchor: (Full: 0); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + } + + Group { + Anchor: (Height: 56, Top: 10); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + Label #Status { + Anchor: (Horizontal: 10, Vertical: 8); + Style: (...$C.@DefaultLabelStyle, FontSize: 14, HorizontalAlignment: Center, VerticalAlignment: Center, Wrap: true); + Text: "Select a slot to withdraw max stack."; + } + } + } + } + } + } + } From 262bbd4f7c77576a5266853f6f0ffc3aa3395980 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sat, 14 Feb 2026 15:03:52 -0500 Subject: [PATCH 02/12] UI changes --- .../storage/ui/TerminalInventoryPage.java | 258 ++-- .../world/events/BlockUseEventSystem.java | 1 + .../UI/Custom/Pages/HytechTerminalPage.ui | 1080 ++--------------- 3 files changed, 228 insertions(+), 1111 deletions(-) diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index 2566f27..136122c 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -5,6 +5,7 @@ import com.github.hytech.storage.network.device.DeviceServerStorage; import com.github.hytech.storage.network.device.DeviceTerminal; import com.github.hytech.storage.state.StateManager; +import com.github.hytech.storage.utility.Logger; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; @@ -19,6 +20,7 @@ import com.hypixel.hytale.server.core.inventory.ItemStack; import com.hypixel.hytale.server.core.inventory.container.ItemContainer; import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackTransaction; import com.hypixel.hytale.server.core.ui.ItemGridSlot; import com.hypixel.hytale.server.core.ui.builder.EventData; import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; @@ -30,29 +32,30 @@ import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** * Interactive custom terminal page used to withdraw items without vanilla drag behavior. */ public class TerminalInventoryPage extends InteractiveCustomUIPage { private static final String LAYOUT_PATH = "Pages/HytechTerminalPage.ui"; - private static final int SLOT_BUTTON_COUNT = 40; private static final int PLAYER_STORAGE_VISIBLE_SLOTS = 30; private static final int PLAYER_HOTBAR_VISIBLE_SLOTS = 10; private static final String ACTION_TAKE = "Take"; - private static final String ACTION_REFRESH = "Refresh"; - private static final String ACTION_DEPOSIT_HELD = "DepositHeld"; - private static final String ACTION_DEPOSIT_HOTBAR = "DepositHotbar"; - private static final String ACTION_DEPOSIT_INVENTORY = "DepositInventory"; private static final String ACTION_DEPOSIT_PLAYER_SLOT = "DepositPlayerSlot"; + private static final String ACTION_TAKE_ALL_TOP = "TakeAllTop"; + private static final String ACTION_PULL_ALL_TOP = "PullAllTop"; private static final String ACTION_SORT_NAME = "SortName"; private static final String ACTION_SORT_COUNT = "SortCount"; + private static final Map SORT_PREFERENCES = new HashMap<>(); private final Vector3i terminalPosition; + private final UUID playerUuid; private String searchQuery; private SortField sortField; private boolean sortDescending; @@ -66,9 +69,14 @@ public class TerminalInventoryPage extends InteractiveCustomUIPage new SortPreference(SortField.COUNT, true) + ); + this.sortField = preference.field; + this.sortDescending = preference.descending; } /** @@ -83,7 +91,7 @@ public void build( ) { commandBuilder.append(LAYOUT_PATH); bindEvents(eventBuilder); - updateSlotLabels(commandBuilder, resolveSlots(resolveNetwork()), "Select a slot to withdraw max stack."); + updateSlotLabels(commandBuilder, resolveSlots(resolveNetwork())); updatePlayerInventoryGrids(commandBuilder, ref, store); commandBuilder.set("#SearchInput.Value", searchQuery); updateControlLabels(commandBuilder); @@ -113,28 +121,18 @@ public void handleDataEvent( return; } - if (ACTION_REFRESH.equalsIgnoreCase(data.type)) { - refresh(playerRef, store, "Refreshed."); - return; - } - - if (ACTION_DEPOSIT_HELD.equalsIgnoreCase(data.type)) { - refresh(playerRef, store, depositHeld(playerRef, store)); - return; - } - - if (ACTION_DEPOSIT_HOTBAR.equalsIgnoreCase(data.type)) { - refresh(playerRef, store, depositFromContainer(playerRef, store, true, false)); + if (ACTION_DEPOSIT_PLAYER_SLOT.equalsIgnoreCase(data.type) && data.slot != null) { + refresh(playerRef, store, depositSinglePlayerSlot(playerRef, store, data.slot)); return; } - if (ACTION_DEPOSIT_INVENTORY.equalsIgnoreCase(data.type)) { - refresh(playerRef, store, depositFromContainer(playerRef, store, false, true)); + if (ACTION_TAKE_ALL_TOP.equalsIgnoreCase(data.type)) { + refresh(playerRef, store, takeAllToPlayerInventory(playerRef, store)); return; } - if (ACTION_DEPOSIT_PLAYER_SLOT.equalsIgnoreCase(data.type) && data.slot != null) { - refresh(playerRef, store, depositSinglePlayerSlot(playerRef, store, data.slot)); + if (ACTION_PULL_ALL_TOP.equalsIgnoreCase(data.type)) { + refresh(playerRef, store, depositFromContainer(playerRef, store, true, true)); return; } @@ -150,8 +148,8 @@ public void handleDataEvent( return; } - if (ACTION_TAKE.equalsIgnoreCase(data.type) && data.slot != null) { - int slotIndex = parseSlotIndex(data.slot); + if (ACTION_TAKE.equalsIgnoreCase(data.type)) { + int slotIndex = parseSlotIndex(resolveSlotPayload(data)); String status = slotIndex >= 0 ? withdrawFromSlot(playerRef, store, slotIndex) : "Invalid slot selection."; @@ -163,17 +161,12 @@ public void handleDataEvent( } /** - * Registers button events for slot actions and refresh. + * Registers button events for slot and control actions. */ private void bindEvents(UIEventBuilder eventBuilder) { - for (int slot = 0; slot < SLOT_BUTTON_COUNT; slot++) { - eventBuilder.addEventBinding( - CustomUIEventBindingType.Activating, - "#Slot" + slot, - EventData.of("Type", ACTION_TAKE).append("Slot", String.valueOf(slot)), - false - ); - } + // NOTE: ItemGrid-level binding currently causes "Failed to apply CustomUI event bindings" + // in this client build. Keep network withdrawal on explicit controls until we switch to + // a selector format the client accepts. for (int slot = 0; slot < PLAYER_STORAGE_VISIBLE_SLOTS; slot++) { eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, @@ -193,26 +186,14 @@ private void bindEvents(UIEventBuilder eventBuilder) { eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, - "#Refresh", - EventData.of("Type", ACTION_REFRESH), - false - ); - eventBuilder.addEventBinding( - CustomUIEventBindingType.Activating, - "#DepositHeld", - EventData.of("Type", ACTION_DEPOSIT_HELD), - false - ); - eventBuilder.addEventBinding( - CustomUIEventBindingType.Activating, - "#DepositHotbar", - EventData.of("Type", ACTION_DEPOSIT_HOTBAR), + "#TakeAllTop", + EventData.of("Type", ACTION_TAKE_ALL_TOP), false ); eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, - "#DepositInventory", - EventData.of("Type", ACTION_DEPOSIT_INVENTORY), + "#PullAllTop", + EventData.of("Type", ACTION_PULL_ALL_TOP), false ); eventBuilder.addEventBinding( @@ -239,6 +220,9 @@ private void bindEvents(UIEventBuilder eventBuilder) { * Parses slot text payload into an integer slot index. */ private int parseSlotIndex(String slot) { + if (slot == null) { + return -1; + } try { return Integer.parseInt(slot); } catch (NumberFormatException ignored) { @@ -246,6 +230,22 @@ private int parseSlotIndex(String slot) { } } + /** + * Resolves slot index payload from known UI event key variants. + */ + private String resolveSlotPayload(TerminalEventData data) { + if (!isNullOrBlank(data.slot)) { + return data.slot; + } + if (!isNullOrBlank(data.slotIndex)) { + return data.slotIndex; + } + if (!isNullOrBlank(data.selectedSlot)) { + return data.selectedSlot; + } + return null; + } + /** * Checks whether a string is null or only whitespace. */ @@ -258,7 +258,7 @@ private boolean isNullOrBlank(String value) { */ private void refresh(Ref playerRef, Store store, String status) { UICommandBuilder commands = new UICommandBuilder(); - updateSlotLabels(commands, resolveSlots(resolveNetwork()), status); + updateSlotLabels(commands, resolveSlots(resolveNetwork())); updatePlayerInventoryGrids(commands, playerRef, store); updateControlLabels(commands); sendUpdate(commands, null, false); @@ -280,10 +280,19 @@ private void updateControlLabels(UICommandBuilder commands) { private void applySortSelection(SortField selectedField) { if (sortField == selectedField) { sortDescending = !sortDescending; + persistSortPreference(); return; } sortField = selectedField; sortDescending = selectedField == SortField.COUNT; + persistSortPreference(); + } + + /** + * Persists in-memory sort preference for the currently viewing player. + */ + private void persistSortPreference() { + SORT_PREFERENCES.put(playerUuid, new SortPreference(sortField, sortDescending)); } /** @@ -325,9 +334,14 @@ private String withdrawFromSlot(Ref playerRef, Store s } /** - * Deposits the currently held hotbar stack into network storage. + * Deposits all stacks from selected inventory sections into network storage. */ - private String depositHeld(Ref playerRef, Store store) { + private String depositFromContainer( + Ref playerRef, + Store store, + boolean includeHotbar, + boolean includeStorage + ) { Network network = resolveNetwork(); if (network == null) { return "Terminal is not connected to a network."; @@ -339,42 +353,27 @@ private String depositHeld(Ref playerRef, Store store) } Inventory inventory = player.getInventory(); - ItemContainer hotbar = inventory.getHotbar(); - short slot = (short) inventory.getActiveHotbarSlot(); - if (slot < 0 || slot >= hotbar.getCapacity()) { - return "No held item."; - } + int moved = 0; - ItemStack held = hotbar.getItemStack(slot); - if (held == null || held.isEmpty()) { - return "No held item."; + if (includeHotbar) { + moved += depositFromItemContainer(network, inventory.getHotbar()); } - - int accepted = addIntoNetworkStorage(network, held.getItemId(), held.getQuantity()); - if (accepted <= 0) { - return "Network is full."; + if (includeStorage) { + moved += depositFromItemContainer(network, inventory.getStorage()); } - int remaining = held.getQuantity() - accepted; - if (remaining > 0) { - hotbar.setItemStackForSlot(slot, new ItemStack(held.getItemId(), remaining)); - } else { - hotbar.setItemStackForSlot(slot, ItemStack.EMPTY); + if (moved <= 0) { + return "Nothing deposited."; } StateManager.getInstance().save(); - return "Deposited " + accepted + " of " + held.getItemId() + "."; + return "Deposited " + moved + " items."; } /** - * Deposits all stacks from selected inventory sections into network storage. + * Withdraws all possible items from the terminal network into player inventory. */ - private String depositFromContainer( - Ref playerRef, - Store store, - boolean includeHotbar, - boolean includeStorage - ) { + private String takeAllToPlayerInventory(Ref playerRef, Store store) { Network network = resolveNetwork(); if (network == null) { return "Terminal is not connected to a network."; @@ -385,22 +384,45 @@ private String depositFromContainer( return "Player unavailable."; } - Inventory inventory = player.getInventory(); - int moved = 0; - - if (includeHotbar) { - moved += depositFromItemContainer(network, inventory.getHotbar()); + ItemContainer destination = player.getInventory().getCombinedEverything(); + Map totals = getNetworkTotals(network); + if (totals.isEmpty()) { + return "Terminal storage is empty."; } - if (includeStorage) { - moved += depositFromItemContainer(network, inventory.getStorage()); + + int movedTotal = 0; + for (Map.Entry entry : totals.entrySet()) { + String itemId = entry.getKey(); + int remainingForItem = entry.getValue(); + int movedForItem = 0; + + while (remainingForItem > 0) { + int maxStack = resolveMaxStackSize(itemId); + int request = Math.min(remainingForItem, maxStack); + ItemStackTransaction tx = destination.addItemStack(new ItemStack(itemId, request)); + ItemStack remainder = tx.getRemainder(); + int remainderCount = (remainder == null || remainder.isEmpty()) ? 0 : remainder.getQuantity(); + int accepted = request - remainderCount; + if (accepted <= 0) { + break; + } + + movedForItem += accepted; + remainingForItem -= accepted; + } + + if (movedForItem > 0) { + removeFromNetworkStorage(network, itemId, movedForItem); + movedTotal += movedForItem; + } } - if (moved <= 0) { - return "Nothing deposited."; + if (movedTotal <= 0) { + return "Inventory is full."; } StateManager.getInstance().save(); - return "Deposited " + moved + " items."; + return "Moved " + movedTotal + " items to player inventory."; } /** @@ -543,20 +565,13 @@ private void removeFromNetworkStorage(Network network, String itemId, int amount } /** - * Resolves merged network item totals and maps the first rows to page slots. + * Resolves merged and filtered network items for the network grid. */ private List resolveSlots(Network network) { - List visible = new ArrayList<>(); if (network == null) { - return visible; - } - - List all = getFilteredItems(network); - int end = Math.min(all.size(), SLOT_BUTTON_COUNT); - for (int i = 0; i < end; i++) { - visible.add(all.get(i)); + return new ArrayList<>(); } - return visible; + return getFilteredItems(network); } /** @@ -624,16 +639,14 @@ private void writeNetworkTotals(Network network, Map totals) { } /** - * Writes display text into slot buttons and status label. + * Writes display item data into the dynamic network grid. */ - private void updateSlotLabels(UICommandBuilder commands, List slots, String status) { - for (int i = 0; i < SLOT_BUTTON_COUNT; i++) { - ItemGridSlot[] slotData = i < slots.size() - ? new ItemGridSlot[]{new ItemGridSlot(new ItemStack(slots.get(i).itemId, slots.get(i).count))} - : new ItemGridSlot[]{new ItemGridSlot()}; - commands.set("#SlotGrid" + i + ".Slots", slotData); - } - commands.set("#Status.Text", status); + private void updateSlotLabels(UICommandBuilder commands, List slots) { + List slotData = new ArrayList<>(); + for (ItemView slot : slots) { + slotData.add(new ItemGridSlot(new ItemStack(slot.itemId, slot.count))); + } + commands.set("#NetworkGrid.Slots", slotData); } /** @@ -710,12 +723,27 @@ private enum SortField { COUNT } + /** + * In-memory sort state for a player's terminal view. + */ + private static final class SortPreference { + private final SortField field; + private final boolean descending; + + private SortPreference(SortField field, boolean descending) { + this.field = field; + this.descending = descending; + } + } + /** * Typed UI event payload decoded from custom page events. */ public static final class TerminalEventData { private static final String KEY_TYPE = "Type"; private static final String KEY_SLOT = "Slot"; + private static final String KEY_SLOT_INDEX = "SlotIndex"; + private static final String KEY_SELECTED_SLOT = "SelectedSlot"; private static final String KEY_SEARCH_QUERY = "@SearchQuery"; public static final BuilderCodec CODEC = @@ -730,6 +758,16 @@ public static final class TerminalEventData { (data, value) -> data.slot = value, data -> data.slot ).add() + .append( + new KeyedCodec<>(KEY_SLOT_INDEX, Codec.STRING), + (data, value) -> data.slotIndex = value, + data -> data.slotIndex + ).add() + .append( + new KeyedCodec<>(KEY_SELECTED_SLOT, Codec.STRING), + (data, value) -> data.selectedSlot = value, + data -> data.selectedSlot + ).add() .append( new KeyedCodec<>(KEY_SEARCH_QUERY, Codec.STRING), (data, value) -> data.searchQuery = value, @@ -739,6 +777,8 @@ public static final class TerminalEventData { private String type; private String slot; + private String slotIndex; + private String selectedSlot; private String searchQuery; /** @@ -747,6 +787,8 @@ public static final class TerminalEventData { public TerminalEventData() { this.type = null; this.slot = null; + this.slotIndex = null; + this.selectedSlot = null; this.searchQuery = null; } } diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java index 45b6207..ff4c3a0 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockUseEventSystem.java @@ -36,6 +36,7 @@ public BlockUseEventSystem() { * Opens the terminal custom page for terminal block interactions. */ @Override + @SuppressWarnings("removal") public void handle( int var1, @NonNullDecl ArchetypeChunk archetypeChunk, diff --git a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui index f480b5c..6bfb8df 100644 --- a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui +++ b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui @@ -1,8 +1,8 @@ $C = "../Common.ui"; -@SlotSize = 46; -@SlotGap = 6; -@GridWidth = 514; +@SlotSize = 72; +@SlotGap = 8; +@GridWidth = 792; Group #TerminalRoot { Anchor: (Full: 0); @@ -12,1021 +12,104 @@ Group #TerminalRoot { LayoutMode: Center; $C.@Container #TerminalWindow { - Anchor: (Width: 820, Height: 780); + Anchor: (Width: 1120, Height: 1100); #Title { Group { - LayoutMode: Top; - Anchor: (Top: 6); - Label { - Text: "HYTECH TERMINAL"; - Style: (FontSize: 28, RenderUppercase: true, RenderBold: true, HorizontalAlignment: Center, VerticalAlignment: Center, TextColor: #d0d9e4); - } - Label { - Anchor: (Top: 6, Height: 22); - Text: "Terminal storage above, player inventory below."; - Style: (FontSize: 14, HorizontalAlignment: Center, VerticalAlignment: Center, TextColor: #8fa2b5); - } - } - } - - #Content { - Group { - LayoutMode: Top; - Anchor: (Top: 8, Bottom: 10); - - Group { - Anchor: (Height: 56); - LayoutMode: Top; - Label { - Anchor: (Height: 18); - Text: "Search Item Id"; - Style: (...$C.@DefaultLabelStyle, FontSize: 13, RenderUppercase: true); - } - Group { - Anchor: (Height: 34, Top: 4); - Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); - TextField #SearchInput { - Anchor: (Horizontal: 8, Vertical: 5); - Value: ""; - } - } - } - - Label { - Anchor: (Top: 10, Height: 20); - Text: "Terminal Network"; - Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); - } - - Group { - Anchor: (Top: 6, Height: 232); - LayoutMode: Center; - Group { - Anchor: (Width: @GridWidth, Height: 232); - LayoutMode: Top; - Group { - Anchor: (Height: @SlotSize, Top: 0); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); - LayoutMode: Full; - ItemGrid #SlotGrid0 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot0 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid1 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot1 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid2 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot2 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid3 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot3 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid4 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot4 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid5 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot5 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid6 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot6 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid7 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot7 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid8 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot8 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid9 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot9 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - } - Group { - Anchor: (Height: @SlotSize, Top: 6); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); - LayoutMode: Full; - ItemGrid #SlotGrid10 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot10 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid11 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot11 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid12 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot12 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid13 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot13 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid14 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot14 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid15 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot15 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid16 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot16 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid17 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot17 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid18 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot18 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid19 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot19 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - } - Group { - Anchor: (Height: @SlotSize, Top: 6); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); - LayoutMode: Full; - ItemGrid #SlotGrid20 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot20 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid21 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot21 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid22 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot22 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid23 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot23 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid24 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot24 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid25 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot25 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid26 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot26 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid27 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot27 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid28 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot28 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid29 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot29 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - } - Group { - Anchor: (Height: @SlotSize, Top: 6); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); - LayoutMode: Full; - ItemGrid #SlotGrid30 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot30 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid31 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot31 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid32 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot32 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid33 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot33 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid34 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot34 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid35 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot35 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid36 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot36 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid37 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #Slot37 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } + LayoutMode: Full; + Anchor: (Height: 24); Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid38 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); + Anchor: (Top: 0, Height: 24, Left: 10, Width: 250); + LayoutMode: Left; + $C.@SmallSecondaryTextButton #SortByName { + Anchor: (Width: 120, Height: 24); + Text: "Name"; } - Button #Slot38 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); + $C.@SmallSecondaryTextButton #SortByCount { + Anchor: (Width: 120, Height: 24, Left: 10); + Text: "Count"; } } Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #SlotGrid39 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); + Anchor: (Top: 0, Height: 24, Right: 10, Width: 94); + LayoutMode: Left; + $C.@SmallSecondaryTextButton #TakeAllTop { + Anchor: (Width: 44, Height: 24); + Text: "<"; + TooltipText: "Take all - Move all items from the terminal to your inventory. (Q)"; + TextTooltipStyle: $C.@DefaultTextTooltipStyle; } - Button #Slot39 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); + $C.@SmallSecondaryTextButton #PullAllTop { + Anchor: (Width: 44, Height: 24, Left: 6); + Text: ">"; + TooltipText: "Pull all - Move all items from your inventory to the terminal. (E)"; + TextTooltipStyle: $C.@DefaultTextTooltipStyle; } } } } + + #Content { + Group { + LayoutMode: Top; + Anchor: (Top: 0, Bottom: 10); + + Group { + Anchor: (Height: 56); + LayoutMode: Top; + Label { + Anchor: (Height: 18); + Text: "Search Item Id"; + Style: (...$C.@DefaultLabelStyle, FontSize: 13, RenderUppercase: true); + } + Group { + Anchor: (Height: 34, Top: 4); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + TextField #SearchInput { + Anchor: (Horizontal: 8, Vertical: 5); + Value: ""; + } + } + } + + Label { + Anchor: (Top: 10, Height: 20); + Text: "NETWORK INVENTORY"; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); } Group { - Anchor: (Height: 32, Top: 8); - LayoutMode: Left; - $C.@SmallSecondaryTextButton #DepositHeld { Anchor: (Width: 170); Text: "Deposit Held"; } - $C.@SmallSecondaryTextButton #DepositHotbar { Anchor: (Width: 170, Left: 8); Text: "Deposit Hotbar"; } - $C.@SmallSecondaryTextButton #DepositInventory { Anchor: (Width: 170, Left: 8); Text: "Deposit Inventory"; } - $C.@SmallSecondaryTextButton #Refresh { Anchor: (Width: 100, Left: 8); Text: "Refresh"; } - $C.@SmallSecondaryTextButton #SortByName { Anchor: (Width: 120, Left: 8); Text: "Name (asc)"; } - $C.@SmallSecondaryTextButton #SortByCount { Anchor: (Width: 130, Left: 8); Text: "Count"; } + Anchor: (Top: 6, Height: 444); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + Group #NetworkScroll { + Anchor: (Horizontal: 6, Vertical: 6); + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + ItemGrid #NetworkGrid { + Anchor: (Width: @GridWidth, Height: 4000); + SlotsPerRow: 10; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: @SlotGap, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + } } Label { - Anchor: (Top: 12, Height: 20); - Text: "Player Inventory (Click slot to deposit)"; + Anchor: (Top: 45, Height: 20); + Text: "INVENTORY"; Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); } Group { - Anchor: (Top: 6, Height: 156); + Anchor: (Top: 10, Height: 250); Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); LayoutMode: Center; Group { - Anchor: (Width: @GridWidth, Height: 138); + Anchor: (Width: @GridWidth, Height: 232); LayoutMode: Top; Group { Anchor: (Height: @SlotSize, Top: 0); @@ -1734,11 +817,11 @@ Group #TerminalRoot { } Group { - Anchor: (Top: 8, Height: 62); + Anchor: (Top: 12, Height: 88); Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); LayoutMode: Center; Group { - Anchor: (Width: @GridWidth, Height: 46); + Anchor: (Width: @GridWidth, Height: @SlotSize); LayoutMode: Left; Group { Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); @@ -1973,15 +1056,6 @@ Group #TerminalRoot { } } - Group { - Anchor: (Height: 56, Top: 10); - Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); - Label #Status { - Anchor: (Horizontal: 10, Vertical: 8); - Style: (...$C.@DefaultLabelStyle, FontSize: 14, HorizontalAlignment: Center, VerticalAlignment: Center, Wrap: true); - Text: "Select a slot to withdraw max stack."; - } - } } } } From 4fc4d9d98d38eb92f72d532a4c2891d05fd3e816 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 00:41:55 -0500 Subject: [PATCH 03/12] UI changes --- README.md | 3 + agents/hytale-modder.agent.md | 162 ++ skills/agent-file-specs/SKILL.md | 97 + skills/agent-file-specs/references/AGENTS.md | 227 ++ .../references/FILE-LOCATIONS.md | 162 ++ .../references/INSTRUCTIONS.md | 289 ++ skills/agent-file-specs/references/PROMPTS.md | 226 ++ .../agent-file-specs/references/SETTINGS.md | 106 + skills/agent-file-specs/references/SKILLS.md | 294 ++ skills/analyze-agent-overlap/SKILL.md | 198 ++ skills/copilot-file-specs/SKILL.md | 476 ++++ skills/curseforge-maven/SKILL.md | 71 + .../scripts/add-curseforge-mod.ps1 | 158 ++ skills/generate-agent-docs/SKILL.md | 266 ++ skills/hytale-blocks/SKILL.md | 197 ++ skills/hytale-camera-controls/SKILL.md | 233 ++ skills/hytale-chat-formatting/SKILL.md | 349 +++ skills/hytale-commands/SKILL.md | 463 +++ skills/hytale-config-files/SKILL.md | 307 ++ skills/hytale-ecs/SKILL.md | 631 +++++ skills/hytale-entity-effects/SKILL.md | 945 +++++++ skills/hytale-env-setup/SKILL.md | 413 +++ skills/hytale-events/SKILL.md | 526 ++++ skills/hytale-hotbar-actions/SKILL.md | 477 ++++ skills/hytale-instances/SKILL.md | 331 +++ skills/hytale-inventory/SKILL.md | 316 +++ skills/hytale-items/SKILL.md | 598 ++++ skills/hytale-logging/SKILL.md | 191 ++ skills/hytale-notifications/SKILL.md | 309 ++ skills/hytale-npc-templates/SKILL.md | 1240 ++++++++ skills/hytale-permissions/SKILL.md | 495 ++++ skills/hytale-persistent-data/SKILL.md | 438 +++ skills/hytale-player-death-event/SKILL.md | 198 ++ skills/hytale-player-input/SKILL.md | 629 +++++ skills/hytale-player-stats/SKILL.md | 236 ++ skills/hytale-playing-sounds/SKILL.md | 311 ++ skills/hytale-plugin-config/SKILL.md | 295 ++ skills/hytale-prefabs/SKILL.md | 102 + skills/hytale-spawning-entities/SKILL.md | 330 +++ skills/hytale-spawning-npcs/SKILL.md | 265 ++ skills/hytale-tag-system/SKILL.md | 272 ++ skills/hytale-teleporting-players/SKILL.md | 189 ++ skills/hytale-text-holograms/SKILL.md | 335 +++ skills/hytale-ui-modding/SKILL.md | 53 + skills/hytale-ui-modding/references/INDEX.md | 66 + .../references/assets-and-packaging.md | 30 + .../references/common-styling.md | 141 + skills/hytale-ui-modding/references/events.md | 216 ++ .../hytale-ui-modding/references/examples.md | 566 ++++ .../hytale-ui-modding/references/java-api.md | 527 ++++ skills/hytale-ui-modding/references/layout.md | 526 ++++ skills/hytale-ui-modding/references/markup.md | 273 ++ .../hytale-ui-modding/references/overview.md | 128 + .../references/translations.md | 184 ++ .../references/troubleshooting.md | 11 + .../references/type-documentation.md | 225 ++ skills/hytale-ui-modding/references/types.md | 779 +++++ skills/hytale-world-gen/SKILL.md | 497 ++++ .../references/biome-editing-guide.md | 107 + .../references/curve-types.md | 87 + .../references/density-nodes.md | 260 ++ .../references/prop-placement-nodes.md | 230 ++ .../references/terrain-nodes.md | 146 + .../references/world-gen-concepts.md | 62 + skills/update-hytale-skills/SKILL.md | 584 ++++ skills/update-server-lib/SKILL.md | 125 + .../scripts/Download-Server.cmd | 137 + .../update-server-lib/scripts/Full-Update.cmd | 51 + .../update-server-lib/scripts/Update-Lib.cmd | 327 +++ skills/validate-agent-files/SKILL.md | 203 ++ .../storage/ui/TerminalInventoryPage.java | 391 ++- .../UI/Custom/Pages/HytechTerminalPage.ui | 2509 ++++++++++++----- 72 files changed, 23057 insertions(+), 740 deletions(-) create mode 100644 agents/hytale-modder.agent.md create mode 100644 skills/agent-file-specs/SKILL.md create mode 100644 skills/agent-file-specs/references/AGENTS.md create mode 100644 skills/agent-file-specs/references/FILE-LOCATIONS.md create mode 100644 skills/agent-file-specs/references/INSTRUCTIONS.md create mode 100644 skills/agent-file-specs/references/PROMPTS.md create mode 100644 skills/agent-file-specs/references/SETTINGS.md create mode 100644 skills/agent-file-specs/references/SKILLS.md create mode 100644 skills/analyze-agent-overlap/SKILL.md create mode 100644 skills/copilot-file-specs/SKILL.md create mode 100644 skills/curseforge-maven/SKILL.md create mode 100644 skills/curseforge-maven/scripts/add-curseforge-mod.ps1 create mode 100644 skills/generate-agent-docs/SKILL.md create mode 100644 skills/hytale-blocks/SKILL.md create mode 100644 skills/hytale-camera-controls/SKILL.md create mode 100644 skills/hytale-chat-formatting/SKILL.md create mode 100644 skills/hytale-commands/SKILL.md create mode 100644 skills/hytale-config-files/SKILL.md create mode 100644 skills/hytale-ecs/SKILL.md create mode 100644 skills/hytale-entity-effects/SKILL.md create mode 100644 skills/hytale-env-setup/SKILL.md create mode 100644 skills/hytale-events/SKILL.md create mode 100644 skills/hytale-hotbar-actions/SKILL.md create mode 100644 skills/hytale-instances/SKILL.md create mode 100644 skills/hytale-inventory/SKILL.md create mode 100644 skills/hytale-items/SKILL.md create mode 100644 skills/hytale-logging/SKILL.md create mode 100644 skills/hytale-notifications/SKILL.md create mode 100644 skills/hytale-npc-templates/SKILL.md create mode 100644 skills/hytale-permissions/SKILL.md create mode 100644 skills/hytale-persistent-data/SKILL.md create mode 100644 skills/hytale-player-death-event/SKILL.md create mode 100644 skills/hytale-player-input/SKILL.md create mode 100644 skills/hytale-player-stats/SKILL.md create mode 100644 skills/hytale-playing-sounds/SKILL.md create mode 100644 skills/hytale-plugin-config/SKILL.md create mode 100644 skills/hytale-prefabs/SKILL.md create mode 100644 skills/hytale-spawning-entities/SKILL.md create mode 100644 skills/hytale-spawning-npcs/SKILL.md create mode 100644 skills/hytale-tag-system/SKILL.md create mode 100644 skills/hytale-teleporting-players/SKILL.md create mode 100644 skills/hytale-text-holograms/SKILL.md create mode 100644 skills/hytale-ui-modding/SKILL.md create mode 100644 skills/hytale-ui-modding/references/INDEX.md create mode 100644 skills/hytale-ui-modding/references/assets-and-packaging.md create mode 100644 skills/hytale-ui-modding/references/common-styling.md create mode 100644 skills/hytale-ui-modding/references/events.md create mode 100644 skills/hytale-ui-modding/references/examples.md create mode 100644 skills/hytale-ui-modding/references/java-api.md create mode 100644 skills/hytale-ui-modding/references/layout.md create mode 100644 skills/hytale-ui-modding/references/markup.md create mode 100644 skills/hytale-ui-modding/references/overview.md create mode 100644 skills/hytale-ui-modding/references/translations.md create mode 100644 skills/hytale-ui-modding/references/troubleshooting.md create mode 100644 skills/hytale-ui-modding/references/type-documentation.md create mode 100644 skills/hytale-ui-modding/references/types.md create mode 100644 skills/hytale-world-gen/SKILL.md create mode 100644 skills/hytale-world-gen/references/biome-editing-guide.md create mode 100644 skills/hytale-world-gen/references/curve-types.md create mode 100644 skills/hytale-world-gen/references/density-nodes.md create mode 100644 skills/hytale-world-gen/references/prop-placement-nodes.md create mode 100644 skills/hytale-world-gen/references/terrain-nodes.md create mode 100644 skills/hytale-world-gen/references/world-gen-concepts.md create mode 100644 skills/update-hytale-skills/SKILL.md create mode 100644 skills/update-server-lib/SKILL.md create mode 100644 skills/update-server-lib/scripts/Download-Server.cmd create mode 100644 skills/update-server-lib/scripts/Full-Update.cmd create mode 100644 skills/update-server-lib/scripts/Update-Lib.cmd create mode 100644 skills/validate-agent-files/SKILL.md diff --git a/README.md b/README.md index d700bed..d562ba2 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,6 @@ demonstration purposes, and should **NOT** be included in your final build. The example plugin also includes a recipe defined by an asset pack. This recipe allows you to craft 10 dirt into 1 dirt using the crafting window. This is also an example and should be removed before you release the plugin. + +## Hytech Storage TODOs +- Revisit terminal UI layout scaling for desktop resolutions/aspect ratios (for example 1080p, 1440p, ultrawide) and reduce fixed-size assumptions where possible. diff --git a/agents/hytale-modder.agent.md b/agents/hytale-modder.agent.md new file mode 100644 index 0000000..a5c1eae --- /dev/null +++ b/agents/hytale-modder.agent.md @@ -0,0 +1,162 @@ +--- +name: Hytale Modder +description: Expert Hytale modding assistant. Helps build plugins using ECS architecture, data-driven JSON, custom UIs, commands, events, items, NPCs, world generation, and more. Leverages decompiled server source and the full library of Hytale modding skills.\n\n**Examples:**\n\n\nContext: User wants to create a custom item.\nuser: "I need a healing potion item that restores 50 health"\nassistant: "I'll create the item JSON definition, the interaction class, and register it in your plugin. Let me check the item and entity-effects skills for the right patterns."\n\n\n\nContext: User wants to build a custom ECS system.\nuser: "I need a system that damages entities standing in lava"\nassistant: "I'll create a TickingSystem that queries for entities with a position component, checks the block at their feet, and applies damage via CommandBuffer. Let me reference the ECS and events skills."\n\n\n\nContext: User wants to add a custom UI HUD.\nuser: "Can you make a mana bar HUD?"\nassistant: "I'll create the .ui file with the bar markup, the Java HUD class using CustomUIHud, and wire up the player stat binding. Let me check the UI modding and player stats skills."\n\n\n\nContext: User wants to spawn NPCs with custom behavior.\nuser: "I want a merchant NPC that sells items"\nassistant: "I'll set up the NPC template JSON with idle behavior, the spawn command, and an interaction that opens a trade UI. Let me pull from the NPC templates, spawning NPCs, and UI modding skills."\n\n\n\nContext: User wants to create a custom command.\nuser: "Add a /teleport command with permission checks"\nassistant: "I'll create the command class extending AbstractPlayerCommand, add permission nodes, and register it in the plugin. Let me reference the commands and permissions skills."\n +tools: [vscode, execute, read, agent, edit, search, web, todo] +--- + +# Hytale Modder + +You are an expert Hytale plugin developer specializing in building server-side mods using Hytale's ECS architecture, data-driven JSON configuration, and the Hytale modding API. + +## Associated Skills + +Load these skills as needed based on the task at hand. Always check relevant skills before implementing — they contain API references, code examples, and patterns that must be followed. + +### Core Architecture +- `hytale-ecs` — Entity Component System fundamentals (Store, Components, Systems, Queries, CommandBuffer) +- `hytale-persistent-data` — Codec/BuilderCodec serialization, saving player and entity data +- `hytale-events` — Event system (IEvent, IAsyncEvent, EcsEvent), event handlers +- `hytale-tag-system` — Hierarchical tag system, tag-based lookups + +### Entities & NPCs +- `hytale-spawning-entities` — Spawning entities with models (Holder, ModelAsset, Store) +- `hytale-spawning-npcs` — NPC spawning via NPCPlugin, NPC inventory and armor +- `hytale-npc-templates` — JSON-based NPC behavior templates (states, sensors, actions, combat) +- `hytale-entity-effects` — Status effects, buffs, debuffs, DoTs (EffectControllerComponent) + +### Items & Inventory +- `hytale-items` — Custom items, item registry, crafting recipes, interactions +- `hytale-inventory` — Inventory management APIs +- `hytale-hotbar-actions` — Custom hotbar key actions, ability triggers + +### Player Systems +- `hytale-player-stats` — Health, stamina, mana, EntityStatMap +- `hytale-player-input` — Packet interception, PacketAdapters, custom interactions +- `hytale-player-death-event` — Death detection and handling +- `hytale-permissions` — Permission nodes and groups +- `hytale-teleporting-players` — Teleportation APIs + +### World & Environment +- `hytale-world-gen` — Procedural world generation (Zones, Biomes, Caves, node system) +- `hytale-instances` — Instance system for instanced worlds + +### UI & Presentation +- `hytale-ui-modding` — Native .ui files, HUD/page Java API, Common.ui styling +- `hytale-text-holograms` — Floating text via entity nameplates +- `hytale-notifications` — Toast/alert notifications via NotificationUtil +- `hytale-chat-formatting` — Rich text chat messages, TinyMessage + +### Media & Effects +- `hytale-camera-controls` — Camera presets, ServerCameraSettings +- `hytale-playing-sounds` — Sound playback APIs + +### Server & Plugin Infrastructure +- `hytale-commands` — Command registration (AbstractCommand, AbstractPlayerCommand) +- `hytale-logging` — HytaleLogger API +- `hytale-config-files` — Plugin configuration +- `hytale-plugin-config` — Plugin manifest and setup +- `hytale-env-setup` — Development environment setup, VS Code tasks, build & deploy configuration +- `curseforge-maven` — Adding CurseForge mod dependencies + +### Maintenance +- `update-server-lib` — Downloading and decompiling the latest Hytale server +- `update-hytale-skills` — Syncing skills with HytaleModding docs + +--- + +## Core Operating Principles + +### Never Assume +If a Hytale API, component type, or JSON structure is unclear, **look it up** in the decompiled server source (`lib/hytale-server/src/main/java/com/hypixel`) or reference JSON (`lib/Server`). Do not guess API signatures or JSON field names. + +### Understand Intent +When a user asks to "add a feature," dig deeper — what gameplay purpose does it serve? What entities, components, and systems are involved? Understand the full picture before writing code. + +### Challenge When Appropriate +If a request would violate ECS principles (e.g., inheritance over composition, hard-coded values, direct store mutation), push back and suggest the correct pattern. Better to prevent bad architecture than fix it later. + +### Consider Implications +Think about performance (this is a game server — latency is the #1 priority), thread safety (use CommandBuffer), data persistence, and how the feature interacts with existing systems. + +### Clarify Unknowns +If you encounter an unfamiliar Hytale API or pattern, say so. Search the decompiled source, check skills, and ask the user if needed. Never fabricate API calls. + +--- + +## Implementation Rules + +These are **non-negotiable** when writing code for this project: + +### Data-Driven Design +- **NEVER hard-code values.** All game data comes from JSON configuration files. +- Reference `lib/Server` for vanilla Hytale JSON structure and examples. +- Custom data goes under `src/main/resources/Server/Hyforged`. +- Prefer single-file JSON definitions. Avoid multi-file JSON solutions unless logically necessary. +- Avoid enums for data that comes from JSON resources. The system is data-driven. + +### ECS Architecture +- **Composition over inheritance.** Entities are identifiers, Components are pure data, Systems contain logic. +- Use `Store` for component access. Never keep direct entity references — use `Ref`. +- Use `CommandBuffer` for all entity/component mutations (thread safety + ordering). +- Components must implement `Component` (or `ChunkStore` for blocks) with default constructor and `clone()`. +- Components must define a `BuilderCodec` for serialization. +- Register components in `setup()`, systems in `start()`. +- Block plugins must declare `Hytale:EntityModule` and `Hytale:BlockModule` dependencies in `manifest.json`. + +### Localization +- All user-facing text must use translation keys via `Message.translation(...)`. +- Add translations to `src/main/resources/Server/Languages//*.lang`. +- `fallback.lang` is only for locale fallback mappings (e.g., `en-GB = en-US`). + +### Code Quality +- Zero warnings or errors when compiling (ignoring pom.xml warnings). +- Follow existing project code style and patterns. +- Keep systems generic — leverage tags and JSON data wherever possible. + +### Building & Testing +- Use the **build plugin** task to compile. +- Use the **build and deploy** task to compile and copy to the local Hytale server for testing. + +--- + +## First-Run Environment Check + +Before starting any modding task, quickly verify the project environment is set up: + +1. **Check for `.vscode/tasks.json`** — if missing, the dev environment is not configured. +2. **Check for `gradle.properties`** with `hytale.home_path` — if missing, builds will fail. +3. If either is missing, **load the `hytale-env-setup` skill** and follow the First-Time Setup Flow: + - Ask the user where Hytale is installed on their system. + - Derive the Mods folder path for deployment. + - Create `gradle.properties`, `.vscode/tasks.json`, and `.vscode/settings.json`. + - Verify with a test build. +4. Once the environment is confirmed, proceed to the normal workflow below. + +--- + +## Workflow + +When given a modding task: + +1. **Identify relevant skills** — Determine which skills apply and load them for API reference and patterns. +2. **Search server source** — Proactively check `lib/hytale-server/src/main/java/com/hypixel` for relevant APIs, existing components, and patterns. Also check `lib/Server` for JSON structure reference. This may not be available. If not skip. +3. **Review existing code** — Check what's already implemented in `src/` to avoid duplication and ensure consistency. +4. **Check TODOs** — Review any existing TODOs that may relate to the task. +5. **Implement** — Write the Java code, JSON definitions, UI files, and translations needed. +6. **Validate** — Check for compile errors and ensure the implementation follows all rules above. + +--- + +## Reference Locations + +| Resource | Path | +|----------|------| +| Plugin source | `src/main/java/` | +| Plugin resources | `src/main/resources/` | +| Custom game data | `src/main/resources/Server/Hyforged` | +| Plugin manifest | `src/main/resources/manifest.json` | +| Translations | `src/main/resources/Server/Languages/` | +| Decompiled server | `lib/hytale-server/src/main/java/com/hypixel` | +| Vanilla game JSON | `lib/Server` | +| Client UI reference | `lib/UI` | +| Memory bank | `.memory_bank/` | diff --git a/skills/agent-file-specs/SKILL.md b/skills/agent-file-specs/SKILL.md new file mode 100644 index 0000000..cee8d8f --- /dev/null +++ b/skills/agent-file-specs/SKILL.md @@ -0,0 +1,97 @@ +--- +name: agent-file-specs +description: Contains the complete specifications for AI coding assistant customization files including agents, skills, prompts, and instructions. Works with GitHub Copilot, Claude Code, Codex, OpenCode, and other providers. Use this skill when you need to reference the correct file format, required fields, supported attributes, file locations, or VS Code settings for any customization file. Follows the Agent Skills open standard (agentskills.io). +--- + +# AI Coding Assistant Customization File Specifications + +This skill provides the authoritative, up-to-date specifications for all AI +coding assistant customization file types. Each file type has its own dedicated +reference document with complete attribute tables, validation rules, examples, +and best practices. + +> **Standard:** These specs align with the [Agent Skills open standard](https://agentskills.io/specification) +> and the [VS Code Copilot customization documentation](https://code.visualstudio.com/docs/copilot/customization/overview). + +## How to Use This Skill + +1. Read the **Quick Reference** below to identify the file type you need. +2. Follow the link to the dedicated reference document for full details. +3. Use the [File Locations](references/FILE-LOCATIONS.md) reference when you + need to know where files go for a specific provider. +4. Use the [Settings](references/SETTINGS.md) reference for VS Code + configuration. + +> **Important:** Detailed specifications live in the `references/` folder — +> not in this file. This keeps the main skill file lightweight for efficient +> context loading per the +> [progressive disclosure](https://agentskills.io/specification) model. + +--- + +## Quick Reference + +| Type | Extension | Purpose | Reference | +|------|-----------|---------|-----------| +| **Agent** | `.agent.md` / `.md` | Custom AI personas with specialized behaviors, tool restrictions, and handoff workflows | [AGENTS.md](references/AGENTS.md) | +| **Skill** | `SKILL.md` (directory-based) | Reusable, portable capabilities with scripts and resources — follows the Agent Skills open standard | [SKILLS.md](references/SKILLS.md) | +| **Prompt** | `.prompt.md` | Reusable prompt templates invoked as slash commands | [PROMPTS.md](references/PROMPTS.md) | +| **Instruction** | `.instructions.md` | Contextual guidance applied automatically by file type or description | [INSTRUCTIONS.md](references/INSTRUCTIONS.md) | +| **File Locations** | — | Provider folder mapping, directory structure, naming conventions | [FILE-LOCATIONS.md](references/FILE-LOCATIONS.md) | +| **VS Code Settings** | — | Settings that control customization file discovery and behavior | [SETTINGS.md](references/SETTINGS.md) | + +--- + +## Provider Support + +These specifications work across multiple AI coding assistant providers. Each +provider uses its own base folder but the file formats are consistent: + +| Provider | Base Folder | +|----------|-------------| +| GitHub Copilot | `.github/` | +| Claude Code | `.claude/` | +| Codex | `.codex/` | +| OpenCode | `.config/opencode/` | + +See [FILE-LOCATIONS.md](references/FILE-LOCATIONS.md) for the complete +directory structure and provider-specific details. + +--- + +## File Type Selection Guide + +Use this decision tree to choose the right file type: + +- **"I want coding standards applied everywhere"** → Use always-on instructions + (`.github/copilot-instructions.md` or `AGENTS.md`). + See [INSTRUCTIONS.md](references/INSTRUCTIONS.md). + +- **"I want different rules for different file types"** → Use file-based + instructions (`.instructions.md` with `applyTo` patterns). + See [INSTRUCTIONS.md](references/INSTRUCTIONS.md). + +- **"I have a reusable task I run repeatedly"** → Use a prompt file + (`.prompt.md`) invoked as a slash command. + See [PROMPTS.md](references/PROMPTS.md). + +- **"I need a multi-step workflow with scripts and resources"** → Use an Agent + Skill (directory with `SKILL.md`). Portable across tools. + See [SKILLS.md](references/SKILLS.md). + +- **"I need a specialized AI persona with tool restrictions"** → Use a custom + agent (`.agent.md`). Supports handoffs for multi-step workflows. + See [AGENTS.md](references/AGENTS.md). + +--- + +## Authoritative Sources + +- [VS Code Copilot Customization Overview](https://code.visualstudio.com/docs/copilot/customization/overview) +- [VS Code Custom Agents](https://code.visualstudio.com/docs/copilot/customization/custom-agents) +- [VS Code Agent Skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills) +- [VS Code Prompt Files](https://code.visualstudio.com/docs/copilot/customization/prompt-files) +- [VS Code Custom Instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) +- [Agent Skills Specification (agentskills.io)](https://agentskills.io/specification) +- [Awesome Copilot (community examples)](https://github.com/github/awesome-copilot) +- [Anthropic Reference Skills](https://github.com/anthropics/skills) diff --git a/skills/agent-file-specs/references/AGENTS.md b/skills/agent-file-specs/references/AGENTS.md new file mode 100644 index 0000000..0331b14 --- /dev/null +++ b/skills/agent-file-specs/references/AGENTS.md @@ -0,0 +1,227 @@ +# Custom Agent Files (`.agent.md`) + +> **Source:** [VS Code Custom Agents Documentation](https://code.visualstudio.com/docs/copilot/customization/custom-agents) + +Custom agents enable you to configure the AI to adopt different personas +tailored to specific development roles and tasks. Each agent defines its own +behavior, available tools, language model preferences, and can orchestrate +multi-step workflows through handoffs. + +> Custom agents were previously known as "custom chat modes." If you have +> existing `.chatmode.md` files, rename them to `.agent.md`. + +--- + +## File Location + +| Scope | Location | +|-------|----------| +| Workspace | `/agents/*.agent.md` or `/agents/*.md` | +| Sub-agents | `/agents/*.subagent.agent.md` | +| User profile | Current VS Code profile folder (available across workspaces) | +| Organization | GitHub organization level (shared across repos) | + +Additional locations can be configured with the `chat.agentFilesLocations` +setting. + +> **Provider note:** `` is your provider's base folder (`.github/`, +> `.claude/`, `.codex/`, `.config/opencode/`). VS Code detects any `.md` file +> in `.github/agents/` as a custom agent. + +--- + +## File Structure + +```markdown +--- +name: agent-name +description: Brief description shown as placeholder in chat input +user-invokable: true +argument-hint: Optional hint for user input +tools: ['tool1', 'tool2'] +agents: ['*'] +model: Claude Sonnet 4 +disable-model-invocation: false +handoffs: + - label: Button Text + agent: target-agent + prompt: Prompt to send + send: false + model: GPT-5 (copilot) +--- + +[Agent instructions body — Markdown content] +``` + +--- + +## Frontmatter Attributes + +| Attribute | Required | Type | Default | Description | +|-----------|----------|------|---------|-------------| +| `name` | No | string | filename | Agent name. If not specified, the file name is used. | +| `description` | Recommended | string | — | Brief description shown as placeholder text in the chat input field. | +| `user-invokable` | No | boolean | `true` | Whether the agent appears in the agents dropdown. Set to `false` for sub-agents that should only be accessible programmatically or via handoffs. | +| `argument-hint` | No | string | — | Hint text shown in the chat input field to guide users on what to type. | +| `tools` | No | string[] | — | List of tool or tool set names available to this agent. Can include built-in tools, tool sets, MCP tools, or extension-contributed tools. Use `/*` to include all tools from an MCP server. | +| `agents` | No | string[] | — | List of agent names available as subagents. Use `*` for all agents, `[]` for none. **Requires the `agent` tool to be included in `tools`.** | +| `model` | No | string or string[] | selected model | AI model to use. Specify a single model name or a prioritized array (system tries each in order until one is available). | +| `disable-model-invocation` | No | boolean | `false` | Prevents this agent from being invoked as a subagent by other agents. | +| `infer` | No | boolean | — | **Deprecated.** Use `user-invokable` and `disable-model-invocation` instead. Previously controlled both picker visibility and subagent availability. | +| `target` | No | string | — | Target environment: `vscode` or `github-copilot`. | +| `mcp-servers` | No | object | — | MCP server configuration JSON for use with `target: github-copilot`. | +| `handoffs` | No | object[] | — | List of handoff configurations for workflow transitions between agents. See [Handoff Configuration](#handoff-configuration) below. | + +--- + +## Naming Conventions + +| Type | Pattern | Example | +|------|---------|---------| +| User-facing agent | `.agent.md` or `.md` | `planner.agent.md` | +| Sub-agent (workflow component) | `.subagent.agent.md` | `due-diligence.subagent.agent.md` | + +- Sub-agents should set `user-invokable: false` so they don't appear in the + agent picker. +- Any `.md` file in the `/agents/` folder is detected as a custom + agent. + +--- + +## Handoff Configuration + +Handoffs create guided sequential workflows between agents. After a chat +response completes, handoff buttons appear to let users transition to the next +agent with relevant context. + +### Use Cases + +- **Planning → Implementation:** Generate a plan, then hand off to start coding. +- **Implementation → Review:** Complete code, then switch to a review agent. +- **Write Failing Tests → Implement:** Generate tests first, then hand off to + make them pass. + +### Syntax + +```yaml +handoffs: + - label: "Display text for button" + agent: "target-agent-name" + prompt: "Prompt text to send to target agent" + send: false + model: "GPT-5 (copilot)" +``` + +### Handoff Attributes + +| Attribute | Required | Type | Default | Description | +|-----------|----------|------|---------|-------------| +| `label` | Yes | string | — | Display text shown on the handoff button. | +| `agent` | Yes | string | — | Target agent identifier to switch to. | +| `prompt` | No | string | — | Prompt text to send to the target agent. | +| `send` | No | boolean | `false` | Auto-submit the prompt if `true`. If `false`, the prompt is pre-filled only. | +| `model` | No | string | — | Language model for the handoff. Use the format `Model Name (vendor)`, e.g., `GPT-5 (copilot)` or `Claude Sonnet 4.5 (copilot)`. | + +--- + +## Body Content + +The body contains Markdown-formatted instructions that define the agent's +behavior. These instructions are prepended to the user's chat prompt whenever +the agent is selected. + +### Capabilities + +- **Markdown formatting:** Full Markdown including headers, lists, code blocks. +- **File references:** Link to other files with standard Markdown links + (e.g., `[standards](../instructions/react.instructions.md)`). +- **Tool references:** Reference tools with `#tool:` syntax + (e.g., `#tool:githubRepo`). +- **Instruction reuse:** Link to `.instructions.md` files to avoid + duplicating guidelines. + +--- + +## Tool List Priority + +When both a custom agent and a prompt file specify tools, the priority order is: + +1. Tools specified in the prompt file (if any) +2. Tools from the referenced custom agent in the prompt file (if any) +3. Default tools for the selected agent (if any) + +If a specified tool is not available, it is silently ignored. + +--- + +## Example: Planning Agent + +```markdown +--- +name: planner +description: Generate an implementation plan for new features +tools: ['search', 'fetch', 'githubRepo', 'usages'] +model: Claude Sonnet 4 +handoffs: + - label: Implement Plan + agent: agent + prompt: Implement the plan outlined above. + send: false +--- + +# Planning Instructions + +You are in planning mode. Generate implementation plans without making code +edits. + +## Plan Structure + +- **Overview:** Brief description of the feature +- **Requirements:** List of functional and non-functional requirements +- **Implementation Steps:** Detailed, actionable steps +- **Testing:** Required test cases and coverage +``` + +--- + +## Example: Sub-Agent + +```markdown +--- +name: due-diligence +user-invokable: false +description: Deep analysis of requirements and integration points +tools: ['search', 'fetch', 'usages'] +--- + +# Due Diligence Analysis + +You perform deep analysis on requirements before planning begins. +Identify integration points, dependencies, risks, and clarifications needed. +``` + +--- + +## Sharing and Organization + +- **Workspace agents** are stored in `/agents/` and shared via + version control. +- **User profile agents** are stored in the current VS Code profile and + available across all workspaces. +- **Organization agents** are defined at the GitHub organization level and + automatically detected when `github.copilot.chat.organizationCustomAgents.enabled` + is set to `true`. +- Agents can be reused in + [background agents](https://code.visualstudio.com/docs/copilot/agents/background-agents) + and [cloud agents](https://code.visualstudio.com/docs/copilot/agents/cloud-agents). + +--- + +## Diagnostics + +If an agent isn't working as expected: + +1. Select **Configure Custom Agents** from the agents dropdown. +2. Use the chat customization diagnostics view: right-click in the Chat view → + **Diagnostics**. +3. Check for syntax errors, invalid configurations, or loading issues. diff --git a/skills/agent-file-specs/references/FILE-LOCATIONS.md b/skills/agent-file-specs/references/FILE-LOCATIONS.md new file mode 100644 index 0000000..7ea03bb --- /dev/null +++ b/skills/agent-file-specs/references/FILE-LOCATIONS.md @@ -0,0 +1,162 @@ +# File Locations and Directory Structure + +> **Sources:** +> - [VS Code Copilot Customization Overview](https://code.visualstudio.com/docs/copilot/customization/overview) +> - [Agent Skills Specification (agentskills.io)](https://agentskills.io/specification) + +This reference covers where customization files are stored, how provider +directories map, naming conventions, and the overall project layout. + +--- + +## Provider Folder Mapping + +Different AI coding assistant providers use the same file formats but store +them in their own base folder: + +| Provider | Base Folder | Notes | +|----------|-------------|-------| +| GitHub Copilot | `.github/` | Most common; widely documented | +| Claude Code | `.claude/` | Anthropic's Claude in VS Code | +| Codex | `.codex/` | OpenAI Codex-based tools | +| OpenCode | `.config/opencode/` | Open-source alternatives | + +Throughout this documentation, `/` represents your chosen provider's +base folder. Replace with the appropriate directory for your environment. + +--- + +## Complete Directory Structure + +``` +/ # .github/, .claude/, .codex/, etc. +├── copilot-instructions.md # Global always-on instructions (single file) +├── agents/ +│ ├── my-agent.agent.md # User-facing agent +│ ├── another-agent.md # User-facing agent (also valid) +│ └── helper.subagent.agent.md # Sub-agent (not user-invokable) +├── skills/ +│ └── my-skill/ # Each skill is a directory +│ ├── SKILL.md # Required — skill definition +│ ├── scripts/ # Optional — executable code +│ ├── references/ # Optional — additional documentation +│ └── assets/ # Optional — templates, images, data +├── prompts/ +│ └── my-prompt.prompt.md # Prompt template (slash command) +└── instructions/ + └── python.instructions.md # File-based contextual instructions +``` + +### Workspace Root Files + +``` +/ +├── AGENTS.md # Always-on instructions (multi-agent) +└── / + └── AGENTS.md # Nested AGENTS.md (experimental) +``` + +--- + +## File Type Locations + +### Agent Files + +| Scope | Location | Extension | +|-------|----------|-----------| +| Workspace | `/agents/` | `*.agent.md` or `*.md` | +| Sub-agents | `/agents/` | `*.subagent.agent.md` | +| User profile | Current VS Code profile folder | `*.agent.md` | +| Organization | GitHub organization level | `*.agent.md` | +| Custom paths | `chat.agentFilesLocations` setting | — | + +### Skill Directories + +| Scope | Location | +|-------|----------| +| Workspace | `/skills//SKILL.md` | +| Personal | `~/.copilot/skills/`, `~/.claude/skills/`, `~/.agents/skills/` | +| Custom paths | `chat.agentSkillsLocations` setting | + +### Prompt Files + +| Scope | Location | Extension | +|-------|----------|-----------| +| Workspace | `/prompts/` | `*.prompt.md` | +| User profile | `prompts` folder of current VS Code profile | `*.prompt.md` | +| Custom paths | `chat.promptFilesLocations` setting | — | + +### Instruction Files + +| Scope | Location | Extension | +|-------|----------|-----------| +| Workspace | `/instructions/` | `*.instructions.md` | +| User profile | `prompts` folder of current VS Code profile | `*.instructions.md` | +| Custom paths | `chat.instructionsFilesLocations` setting | — | + +--- + +## Naming Conventions + +### Agent Files + +| Type | Pattern | Example | +|------|---------|---------| +| User-facing agent | `.agent.md` | `planner.agent.md` | +| User-facing agent (alt) | `.md` | `planner.md` | +| Sub-agent | `.subagent.agent.md` | `due-diligence.subagent.agent.md` | + +> Any `.md` file in `/agents/` is treated as a custom agent. + +### Skill Directories + +| Rule | Example | +|------|---------| +| Directory name = `name` field | `skills/code-review/` → `name: code-review` | +| Lowercase, hyphens only | `my-skill` (not `My_Skill`) | +| 1–64 characters | `a` through 64 chars max | +| No leading/trailing hyphens | `my-skill` (not `-my-skill`) | +| No consecutive hyphens | `my-skill` (not `my--skill`) | + +### Prompt Files + +| Pattern | Example | +|---------|---------| +| `.prompt.md` | `create-react-form.prompt.md` | + +### Instruction Files + +| Pattern | Example | +|---------|---------| +| `.instructions.md` | `python.instructions.md` | + +--- + +## Tool Reference Syntax + +In all body content (agents, skills, prompts, instructions), reference tools +using: + +``` +#tool: +``` + +Example: `Use #tool:githubRepo to access repository information.` + +--- + +## Settings for Custom Locations + +You can extend the default search paths for all file types: + +| Setting | Default Path | Purpose | +|---------|-------------|---------| +| `chat.agentFilesLocations` | `/agents/` | Additional agent file folders | +| `chat.agentSkillsLocations` | `/skills/` | Additional skill folders | +| `chat.promptFilesLocations` | `/prompts/` | Additional prompt file folders | +| `chat.instructionsFilesLocations` | `/instructions/` | Additional instruction file folders | + +This is useful for: +- Sharing files across projects from a central location +- Keeping personal customizations separate from workspace files +- Organizing large projects with multiple configuration folders diff --git a/skills/agent-file-specs/references/INSTRUCTIONS.md b/skills/agent-file-specs/references/INSTRUCTIONS.md new file mode 100644 index 0000000..811f6e1 --- /dev/null +++ b/skills/agent-file-specs/references/INSTRUCTIONS.md @@ -0,0 +1,289 @@ +# Instruction Files (`.instructions.md`) + +> **Source:** [VS Code Custom Instructions Documentation](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) + +Custom instructions define common guidelines and rules that automatically +influence how AI generates code and handles development tasks. Instead of +manually including context in every chat prompt, specify custom instructions in +Markdown files for consistent, convention-aligned responses. + +--- + +## Types of Instructions + +VS Code supports two categories of custom instructions. When multiple +instruction files exist, VS Code combines them — no specific order is +guaranteed. + +### Always-On Instructions + +Automatically included in every chat request. + +| Type | File | Notes | +|------|------|-------| +| Global instructions | `/copilot-instructions.md` | Single file at provider folder root. Applies to all chat requests automatically. | +| AGENTS.md | `AGENTS.md` at workspace root | Useful for multi-agent workspaces. Enable with `chat.useAgentsMdFile` setting. | +| Nested AGENTS.md | `AGENTS.md` in subfolders | Experimental. Enable with `chat.useNestedAgentsMdFiles`. VS Code searches recursively. | +| Organization-level | Defined at GitHub org level | Shared across repos. Enable with `github.copilot.chat.organizationInstructions.enabled`. | + +### File-Based Instructions + +Applied dynamically based on file patterns or description matching. + +| Type | File | Notes | +|------|------|-------| +| Pattern-matched | `*.instructions.md` with `applyTo` | Automatically applied when working on files matching the glob pattern. | +| Description-matched | `*.instructions.md` with `description` | Semantically matched to current task based on description content. | +| Manual | `*.instructions.md` without `applyTo` | Not applied automatically; can be manually attached to a chat request. | + +--- + +## File Location + +| Scope | Location | +|-------|----------| +| Workspace | `/instructions/*.instructions.md` | +| User profile | `prompts` folder of the current VS Code profile | + +Additional locations can be configured with the `chat.instructionsFilesLocations` +setting. + +> **Provider note:** `` is your provider's base folder (`.github/`, +> `.claude/`, `.codex/`, `.config/opencode/`). + +--- + +## File Structure + +```markdown +--- +name: Friendly Name +description: What these instructions cover +applyTo: "**/*.ts" +--- + +[Instruction content — Markdown] +``` + +--- + +## Frontmatter Attributes + +| Attribute | Required | Type | Default | Description | +|-----------|----------|------|---------|-------------| +| `name` | No | string | filename | Display name shown in the UI. | +| `description` | No | string | — | Short description shown on hover. Also used for semantic matching to determine when to apply instructions. | +| `applyTo` | No* | string or string[] | — | Glob pattern(s) for automatic application. Patterns are relative to workspace root. | + +> *If `applyTo` is not specified, instructions are not applied automatically. +> They can still be manually attached to a chat request or matched via +> description. + +--- + +## ApplyTo Patterns + +| Pattern | Matches | +|---------|---------| +| `"**/*.ts"` | All TypeScript files in the workspace | +| `"**/*.py"` | All Python files | +| `["**/*.ts", "**/*.tsx"]` | TypeScript and TSX files | +| `"**"` | All files in the workspace | +| `"src/frontend/**"` | All files under `src/frontend/` | +| `"**/test/**/*.ts"` | All TypeScript files in any `test/` directory | + +- Patterns are relative to the workspace root. +- Instructions with `applyTo` are applied when **creating or modifying** files + matching the pattern — not for read operations. +- Enable pattern-based instructions with the `chat.includeApplyingInstructions` + setting. + +--- + +## Body Content + +The body contains guidelines in Markdown format: + +- **Markdown formatting:** Full Markdown including headers, lists, code blocks. +- **Tool references:** Reference tools with `#tool:` syntax. +- **File references:** Link to other files with Markdown links. Enable with + `chat.includeReferencedInstructions`. +- **Code examples:** Show preferred and avoided patterns. + +--- + +## Global Instructions (`copilot-instructions.md`) + +A single `/copilot-instructions.md` file applies to ALL chat requests +in the workspace automatically. + +### When to Use + +- Coding style and naming conventions that apply across the project +- Technology stack declarations and preferred libraries +- Architectural patterns to follow or avoid +- Security requirements and error handling approaches +- Documentation standards + +### Example + +```markdown +# Project Guidelines + +- Use TypeScript strict mode for all files +- Prefer functional components with hooks over class components +- Use date-fns instead of moment.js (moment is deprecated) +- All API responses must follow the ApiResponse interface +- Error handling: always use the custom AppError class +``` + +### Settings + +| Setting | Purpose | +|---------|---------| +| `github.copilot.chat.codeGeneration.useInstructionFiles` | Enable `copilot-instructions.md` | + +--- + +## AGENTS.md + +Place at workspace root for always-on instructions recognized by multiple AI +agents. + +### When to Use + +- You work with multiple AI coding agents and want shared instructions +- You want subfolder-level instructions for different parts of a monorepo + +### Settings + +| Setting | Purpose | +|---------|---------| +| `chat.useAgentsMdFile` | Enable `AGENTS.md` file | +| `chat.useNestedAgentsMdFiles` | Enable nested `AGENTS.md` files in subfolders (experimental) | + +--- + +## Organization-Level Instructions + +Share instructions across multiple workspaces and repositories within a GitHub +organization. + +- Defined at the GitHub organization level +- Automatically detected and shown alongside personal/workspace instructions +- Enable with `github.copilot.chat.organizationInstructions.enabled` + +--- + +## Instruction Priority + +When multiple instruction types exist, all are provided to the AI. Higher +priority takes precedence in conflicts: + +1. **Personal instructions** (user-level) — highest priority +2. **Repository instructions** (`copilot-instructions.md` or `AGENTS.md`) +3. **Organization instructions** — lowest priority + +--- + +## Scenario-Specific Instruction Settings + +Configure custom instructions for specialized scenarios via VS Code settings: + +| Setting | Purpose | +|---------|---------| +| `github.copilot.chat.reviewSelection.instructions` | Code review instructions | +| `github.copilot.chat.commitMessageGeneration.instructions` | Commit message generation | +| `github.copilot.chat.pullRequestDescriptionGeneration.instructions` | PR title/description generation | + +**Format:** Array of objects with `text` (inline) or `file` (reference) +property: + +```json +{ + "github.copilot.chat.reviewSelection.instructions": [ + { "text": "Always check for security vulnerabilities." }, + { "file": "guidance/review-guidelines.md" } + ] +} +``` + +> Support for settings-based instructions may be removed in the future. Prefer +> file-based instructions. + +--- + +## Example: Language-Specific Instructions + +```markdown +--- +name: Python Standards +description: Coding standards for Python files +applyTo: "**/*.py" +--- + +# Python Coding Standards + +- Follow the PEP 8 style guide +- Use type hints for all function signatures +- Write docstrings for all public functions +- Use 4 spaces for indentation +- Prefer f-strings over .format() or % formatting +``` + +--- + +## Example: Framework-Specific Instructions + +```markdown +--- +name: React Conventions +description: React component conventions and patterns +applyTo: ["**/*.tsx", "**/*.jsx"] +--- + +# React Conventions + +- Use functional components with hooks (no class components) +- Use React.FC for component typing +- Keep components under 200 lines; extract sub-components +- Use custom hooks for shared logic (use* prefix) +- Prefer named exports over default exports +``` + +--- + +## Tips for Effective Instructions + +- **Keep instructions short and self-contained.** Each should be a single, + simple statement. +- **Include reasoning behind rules.** The AI makes better decisions when it + understands why (e.g., "Use `date-fns` instead of `moment.js` — moment.js is + deprecated and increases bundle size."). +- **Show preferred and avoided patterns** with concrete code examples. +- **Focus on non-obvious rules.** Skip conventions that linters or formatters + already enforce. +- **Use multiple `.instructions.md` files** per topic with selective `applyTo` + patterns for better organization. +- **Store project-specific instructions in your workspace** to share with team + members via version control. +- **Reuse instructions** by referencing them in prompt files and custom agents + via Markdown links. +- **Whitespace between instructions** is ignored — format for readability. + +--- + +## Diagnostics + +If an instructions file isn't being applied: + +1. Verify the file is in a recognized location (`chat.instructionsFilesLocations`). +2. Check the `applyTo` glob pattern matches the files you're working on. +3. Verify relevant settings are enabled: + - `chat.includeApplyingInstructions` for pattern-based instructions + - `chat.includeReferencedInstructions` for Markdown-linked instructions + - `chat.useAgentsMdFile` for `AGENTS.md` +4. Use the chat customization diagnostics view: right-click in Chat → + **Diagnostics**. +5. Check the References section in the chat response to see which instructions + were applied. diff --git a/skills/agent-file-specs/references/PROMPTS.md b/skills/agent-file-specs/references/PROMPTS.md new file mode 100644 index 0000000..10d675f --- /dev/null +++ b/skills/agent-file-specs/references/PROMPTS.md @@ -0,0 +1,226 @@ +# Prompt Files (`.prompt.md`) + +> **Source:** [VS Code Prompt Files Documentation](https://code.visualstudio.com/docs/copilot/customization/prompt-files) + +Prompt files (also known as slash commands) let you encode common tasks as +standalone Markdown files that you invoke directly in chat. Each prompt file +includes task-specific context, variable placeholders, and guidelines for how +the task should be performed. + +Unlike custom instructions that are applied automatically, prompt files are +invoked manually by typing `/` followed by the prompt name. + +--- + +## File Location + +| Scope | Location | +|-------|----------| +| Workspace | `/prompts/*.prompt.md` | +| User profile | `prompts` folder of the current VS Code profile | + +Additional locations can be configured with the `chat.promptFilesLocations` +setting. + +> **Provider note:** `` is your provider's base folder (`.github/`, +> `.claude/`, `.codex/`, `.config/opencode/`). + +--- + +## File Structure + +```markdown +--- +name: prompt-name +description: What this prompt accomplishes +argument-hint: Guide for user input +agent: agent-name +model: Claude Sonnet 4 +tools: ['tool1', 'tool2'] +--- + +[Prompt template body with variables] +``` + +--- + +## Frontmatter Attributes + +| Attribute | Required | Type | Default | Description | +|-----------|----------|------|---------|-------------| +| `name` | No | string | filename | Prompt name used after `/` in chat. If not specified, the filename is used. | +| `description` | No | string | — | Short description of the prompt. | +| `argument-hint` | No | string | — | Hint text shown in the chat input field to guide users on how to interact with the prompt. | +| `agent` | No | string | current agent | Agent to use for running the prompt: `ask`, `edit`, `agent`, `plan`, or the name of a custom agent. If `tools` are specified and no agent is set, defaults to `agent`. | +| `model` | No | string | selected model | Language model to use when running the prompt. If not specified, uses the currently selected model. | +| `tools` | No | string[] | — | List of tool or tool set names available for this prompt. Can include built-in tools, tool sets, MCP tools, or extension-contributed tools. Use `/*` for all MCP server tools. | + +> If a specified tool is not available when running the prompt, it is silently +> ignored. + +--- + +## Body Content + +The body contains the prompt text in Markdown format. It supports: + +- **Markdown formatting:** Full Markdown including headers, lists, code blocks. +- **File references:** Link to workspace files with relative Markdown links + (paths are relative to the prompt file location). +- **Tool references:** Reference tools with `#tool:` syntax + (e.g., `#tool:githubRepo`). +- **Variables:** Dynamic placeholders using `${variableName}` syntax. + +--- + +## Variables + +Prompt files support several categories of variables: + +### Workspace Variables + +| Variable | Description | +|----------|-------------| +| `${workspaceFolder}` | Full path to the workspace root folder | +| `${workspaceFolderBasename}` | Name of the workspace root folder | + +### Selection Variables + +| Variable | Description | +|----------|-------------| +| `${selection}` | Currently selected text in the editor | +| `${selectedText}` | Same as `${selection}` | + +### File Context Variables + +| Variable | Description | +|----------|-------------| +| `${file}` | Full path to the currently open file | +| `${fileBasename}` | Filename with extension (e.g., `index.ts`) | +| `${fileDirname}` | Directory path of the current file | +| `${fileBasenameNoExtension}` | Filename without extension (e.g., `index`) | + +### Input Variables + +| Variable | Description | +|----------|-------------| +| `${input:varName}` | Prompts user for a value with label `varName` | +| `${input:varName:placeholder}` | Same as above, with placeholder hint text | + +--- + +## Tool List Priority + +When both a prompt file and a custom agent specify tools, the priority is: + +1. Tools specified in the prompt file (highest) +2. Tools from the referenced custom agent in the prompt file +3. Default tools for the selected agent (lowest) + +--- + +## Using Prompt Files + +There are multiple ways to invoke a prompt file: + +1. **Slash command:** Type `/` followed by the prompt name in chat + (e.g., `/create-react-form`). You can append extra input after the command. +2. **Command Palette:** Run `Chat: Run Prompt` and select from the list. +3. **Editor play button:** Open the `.prompt.md` file in the editor and press + the play button. Useful for testing and iterating. + +Use the `chat.promptFilesRecommendations` setting to show prompts as +recommended actions when starting a new chat session. + +--- + +## Example: React Form Component + +```markdown +--- +name: create-react-form +description: Generate a React form component with validation +agent: agent +tools: ['editFiles'] +--- + +# Create React Form Component + +Generate a React form component named ${input:formName:MyForm} with the +following requirements: + +- Use TypeScript +- Include form validation +- Follow project conventions in [coding standards](../instructions/react.instructions.md) + +## Form Fields + +${input:fields:Describe the form fields needed} +``` + +--- + +## Example: Security Review + +```markdown +--- +name: security-review +description: Perform a security review of a REST API endpoint +agent: ask +tools: ['search', 'usages'] +--- + +# Security Review + +Review the selected code for security vulnerabilities: + +${selection} + +## Check For + +- SQL injection and NoSQL injection +- Authentication and authorization bypass +- Input validation issues +- Sensitive data exposure +- Rate limiting concerns + +## Output Format + +Provide findings organized by severity: Critical, High, Medium, Low. +``` + +--- + +## Tips for Effective Prompts + +- Clearly describe what the prompt should accomplish and what output format is + expected. +- Provide examples of expected input and output to guide the AI. +- Use Markdown links to reference custom instructions rather than duplicating + guidelines in each prompt. +- Use built-in variables like `${selection}` and input variables to make prompts + flexible. +- Use the editor play button to quickly test and iterate on prompts. +- Store workspace prompt files in version control to share with team members. + +--- + +## Syncing Across Devices + +User prompt files can be synced across devices using VS Code Settings Sync: + +1. Enable [Settings Sync](https://code.visualstudio.com/docs/configure/settings-sync). +2. Run **Settings Sync: Configure** from the Command Palette. +3. Select **Prompts and Instructions** from the sync list. + +--- + +## Diagnostics + +If a prompt file isn't working: + +1. Use the chat customization diagnostics view: right-click in Chat → + **Diagnostics**. +2. Check that the file is in a recognized location + (`chat.promptFilesLocations`). +3. Verify the YAML frontmatter syntax is valid. diff --git a/skills/agent-file-specs/references/SETTINGS.md b/skills/agent-file-specs/references/SETTINGS.md new file mode 100644 index 0000000..a6a371f --- /dev/null +++ b/skills/agent-file-specs/references/SETTINGS.md @@ -0,0 +1,106 @@ +# VS Code Settings Reference + +> **Source:** [VS Code Copilot Customization Overview](https://code.visualstudio.com/docs/copilot/customization/overview) + +This reference covers all VS Code settings that control the discovery, loading, +and behavior of AI coding assistant customization files (agents, skills, prompts, +and instructions). + +--- + +## Core Discovery Settings + +These settings control where VS Code looks for customization files and whether +they are enabled. + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `github.copilot.chat.codeGeneration.useInstructionFiles` | boolean | `true` | Enable the `/copilot-instructions.md` global instructions file. | +| `chat.instructionsFilesLocations` | string[] | `[".github/instructions"]` | Additional folders where VS Code searches for `*.instructions.md` files. | +| `chat.promptFilesLocations` | string[] | `[".github/prompts"]` | Additional folders where VS Code searches for `*.prompt.md` files. | +| `chat.agentFilesLocations` | string[] | `[".github/agents"]` | Additional folders where VS Code searches for `*.agent.md` files. | +| `chat.agentSkillsLocations` | string[] | — | Additional folders where VS Code searches for skill directories. | +| `chat.useAgentsMdFile` | boolean | — | Enable `AGENTS.md` file at workspace root. | +| `chat.useNestedAgentsMdFiles` | boolean | — | Enable nested `AGENTS.md` files in subfolders (experimental). | +| `chat.useAgentSkills` | boolean | — | Enable skills in `.claude/skills/` or `.github/skills/`. | + +--- + +## Instruction Behavior Settings + +These settings control how instructions are matched and applied. + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `chat.includeApplyingInstructions` | boolean | — | Enable instructions with `applyTo` glob patterns to be applied automatically when matching files are involved. | +| `chat.includeReferencedInstructions` | boolean | — | Enable instructions referenced via Markdown links in other files to be included in context. | + +--- + +## Organization and Sharing Settings + +These settings enable organization-wide customization sharing via GitHub. + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `github.copilot.chat.organizationInstructions.enabled` | boolean | `false` | Enable discovery and use of organization-level custom instructions defined at the GitHub organization level. | +| `github.copilot.chat.organizationCustomAgents.enabled` | boolean | `false` | Enable discovery and use of organization-level custom agents defined at the GitHub organization level. | + +--- + +## Prompt Behavior Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `chat.promptFilesRecommendations` | boolean | — | Show prompts as recommended actions when starting a new chat session. | + +--- + +## Scenario-Specific Instruction Settings + +Configure custom instructions for specific VS Code scenarios. These accept an +array of objects with `text` (inline instruction) or `file` (path to Markdown +file) properties. + +| Setting | Purpose | +|---------|---------| +| `github.copilot.chat.reviewSelection.instructions` | Code review instructions | +| `github.copilot.chat.commitMessageGeneration.instructions` | Commit message generation | +| `github.copilot.chat.pullRequestDescriptionGeneration.instructions` | PR title and description generation | + +### Format + +```json +{ + "github.copilot.chat.reviewSelection.instructions": [ + { "text": "Always check for security vulnerabilities." }, + { "file": "guidance/review-guidelines.md" } + ] +} +``` + +> Settings-based instructions may be removed in the future. Prefer file-based +> instructions where possible. + +--- + +## Sync Settings + +User-level prompt and instruction files can be synced across devices using +[Settings Sync](https://code.visualstudio.com/docs/configure/settings-sync). + +1. Enable Settings Sync. +2. Run **Settings Sync: Configure** from the Command Palette. +3. Select **Prompts and Instructions** from the list. + +--- + +## Diagnostics + +To troubleshoot customization issues: + +1. Select **Configure Chat** (gear icon) → **Diagnostics** in the Chat view. +2. Review all loaded custom agents, prompt files, instruction files, and skills. +3. Check for syntax errors, invalid configurations, or loading issues. +4. See [Troubleshooting AI in VS Code](https://code.visualstudio.com/docs/copilot/troubleshooting) + for more details. diff --git a/skills/agent-file-specs/references/SKILLS.md b/skills/agent-file-specs/references/SKILLS.md new file mode 100644 index 0000000..7b361b1 --- /dev/null +++ b/skills/agent-file-specs/references/SKILLS.md @@ -0,0 +1,294 @@ +# Agent Skill Files (`SKILL.md`) + +> **Sources:** +> - [Agent Skills Specification (agentskills.io)](https://agentskills.io/specification) +> - [VS Code Agent Skills Documentation](https://code.visualstudio.com/docs/copilot/customization/agent-skills) + +Agent Skills are directory-based capabilities that provide the AI with +specialized workflows, scripts, and resources. Skills are an +[open standard](https://agentskills.io/) that works across multiple AI agents +including VS Code, GitHub Copilot CLI, and GitHub Copilot coding agent. + +--- + +## Directory Structure + +A skill is a **directory** containing at minimum a `SKILL.md` file: + +``` +skill-name/ +├── SKILL.md # Required — skill definition and instructions +├── scripts/ # Optional — executable code +├── references/ # Optional — additional documentation +└── assets/ # Optional — static resources (templates, images, data) +``` + +> **Key principle:** Splitting content across subdirectories keeps the main +> `SKILL.md` lightweight and enables progressive disclosure. Avoid dumping +> everything into `SKILL.md` — use `references/`, `scripts/`, and `assets/` +> for detailed content. + +--- + +## File Location + +| Scope | Location | +|-------|----------| +| Workspace (project) | `/skills//SKILL.md` | +| Personal (user profile) | `~/.copilot/skills/`, `~/.claude/skills/`, `~/.agents/skills/` | + +Additional locations can be configured with the `chat.agentSkillsLocations` +setting. + +> **Provider note:** `` is your provider's base folder (`.github/`, +> `.claude/`, `.codex/`, `.config/opencode/`). + +--- + +## SKILL.md Format + +The `SKILL.md` file must contain YAML frontmatter followed by Markdown content. This should ideally be under 5000 tokens in total (frontmatter + body) to ensure efficient loading. Consider moving detailed instructions and resources to `references/` files if you exceed this. + +### Minimal Example + +```markdown +--- +name: skill-name +description: A description of what this skill does and when to use it. +--- + +# Skill Instructions + +Your detailed instructions, guidelines, and examples go here... +``` + +### Full Example with Optional Fields + +```markdown +--- +name: pdf-processing +description: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents or when the user mentions PDFs, forms, or document extraction. +license: Apache-2.0 +compatibility: Requires Python 3.9+ and the PyPDF2 package +metadata: + author: example-org + version: "1.0" +allowed-tools: Bash(python:*) Read +--- + +# PDF Processing + +Step-by-step instructions for working with PDFs... +``` + +--- + +## Frontmatter Attributes + +| Attribute | Required | Constraints | Description | +|-----------|----------|-------------|-------------| +| `name` | **Yes** | 1–64 chars, lowercase `a-z`, `0-9`, `-` only | Unique identifier for the skill. Must match the parent directory name exactly. | +| `description` | **Yes** | 1–1024 chars | Describes what the skill does **and when to use it**. Should include trigger keywords that help agents identify relevant tasks. | +| `license` | No | — | License name or reference to a bundled license file (e.g., `Apache-2.0`, `Proprietary. LICENSE.txt has complete terms`). | +| `compatibility` | No | 1–500 chars if provided | Environment requirements: intended product, required system packages, network access needs, etc. Most skills do not need this. | +| `metadata` | No | key-value map (strings) | Arbitrary key-value pairs for additional metadata. Recommend making key names reasonably unique to avoid conflicts. | +| `allowed-tools` | No | space-delimited list | Pre-approved tools the skill may use. **Experimental** — support varies between agent implementations. | + +--- + +## Name Validation Rules + +The `name` field has strict validation: + +| Rule | Valid | Invalid | +|------|-------|---------| +| Lowercase only | `pdf-processing` | `PDF-Processing` | +| No leading/trailing hyphens | `my-skill` | `-my-skill`, `my-skill-` | +| No consecutive hyphens | `my-skill` | `my--skill` | +| Alphanumeric + hyphens only | `data-analysis` | `data_analysis`, `data.analysis` | +| 1–64 characters | `a` through 64 chars | empty or 65+ chars | +| **Must match directory name** | `skills/my-skill/` → `name: my-skill` | mismatch between dir and name | + +--- + +## Description Best Practices + +The `description` field is critical — it determines when agents activate the +skill. + +**Good description:** +```yaml +description: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents or when the user mentions PDFs, forms, or document extraction. +``` + +**Poor description:** +```yaml +description: Helps with PDFs. +``` + +Include: +- **What** the skill does (capabilities) +- **When** to use it (trigger conditions/keywords) +- Specific action verbs and domain terms + +--- + +## Body Content + +The Markdown body after the frontmatter contains the skill instructions. There +are no format restrictions — write whatever helps agents perform the task +effectively. + +### Recommended Sections + +- **Step-by-step instructions** for the primary workflow +- **Examples** of inputs and outputs +- **Common edge cases** and how to handle them +- **File references** pointing to scripts, templates, or detailed docs + +### Size Guidelines + +- Keep the main `SKILL.md` body **under 500 lines** +- Target **< 5000 tokens** for the body content +- Move detailed reference material to `references/` files +- Move executable code to `scripts/` +- Move templates and data to `assets/` + +--- + +## Optional Directories + +### `scripts/` + +Contains executable code that agents can run. Scripts should: + +- Be self-contained or clearly document dependencies +- Include helpful error messages +- Handle edge cases gracefully + +Supported languages depend on the agent implementation. Common options include +Python, Bash, and JavaScript. + +### `references/` + +Contains additional documentation that agents can read on demand: + +- `REFERENCE.md` — Detailed technical reference +- `FORMS.md` — Form templates or structured data formats +- Domain-specific files (`finance.md`, `legal.md`, etc.) + +Keep individual reference files focused. Agents load these on demand, so +smaller files mean less context usage. + +### `assets/` + +Contains static resources: + +- Templates (document templates, configuration templates) +- Images (diagrams, examples) +- Data files (lookup tables, schemas) + +--- + +## Progressive Disclosure + +Skills use a three-level loading system for efficient context usage: + +| Level | Content | When Loaded | Size Target | +|-------|---------|-------------|-------------| +| **1. Metadata** | `name` and `description` from frontmatter | Always (at startup) | ~100 tokens | +| **2. Instructions** | Full `SKILL.md` body | When skill is activated (description matches task) | < 5000 tokens | +| **3. Resources** | Files in `scripts/`, `references/`, `assets/` | On demand (when agent references them) | As needed | + +This architecture means you can install many skills without consuming context. +Only relevant content loads for each task. + +--- + +## File References + +When referencing files in your skill, use relative paths from the skill root: + +```markdown +See [the reference guide](references/REFERENCE.md) for details. + +Run the extraction script: +scripts/extract.py +``` + +Keep file references **one level deep** from `SKILL.md`. Avoid deeply nested +reference chains. + +--- + +## Validation + +Use the [skills-ref](https://github.com/agentskills/agentskills/tree/main/skills-ref) +reference library to validate your skills: + +```bash +skills-ref validate ./my-skill +``` + +This checks that your `SKILL.md` frontmatter is valid and follows all naming +conventions. + +--- + +## Example: Web Application Testing Skill + +``` +webapp-testing/ +├── SKILL.md +├── scripts/ +│ └── run-tests.sh +├── references/ +│ └── test-patterns.md +└── assets/ + └── test-template.js +``` + +**SKILL.md:** +```markdown +--- +name: webapp-testing +description: Runs and debugs web application tests using Jest and Playwright. Use when testing web apps, writing test cases, or debugging test failures. +--- + +# Web Application Testing + +## Running Tests + +1. Identify the test framework in use (Jest, Playwright, or both) +2. Run the appropriate test command +3. Analyze failures and suggest fixes + +## Test Patterns + +See [test patterns reference](references/test-patterns.md) for common patterns. + +## Writing New Tests + +Use the [test template](assets/test-template.js) as a starting point. +``` + +--- + +## Skills vs Custom Instructions + +| Feature | Agent Skills | Custom Instructions | +|---------|-------------|-------------------| +| Purpose | Specialized capabilities and workflows | Coding standards and guidelines | +| Portability | Open standard — works across VS Code, CLI, coding agent | VS Code and GitHub.com only | +| Content | Instructions, scripts, examples, resources | Instructions only | +| Scope | Task-specific, loaded on-demand | Always applied or via glob patterns | +| Standard | [agentskills.io](https://agentskills.io/) | VS Code-specific | + +--- + +## Community Resources + +- [Agent Skills Specification](https://agentskills.io/specification) +- [Awesome Copilot (community skills)](https://github.com/github/awesome-copilot) +- [Anthropic Reference Skills](https://github.com/anthropics/skills) +- [skills-ref Validation Tool](https://github.com/agentskills/agentskills/tree/main/skills-ref) diff --git a/skills/analyze-agent-overlap/SKILL.md b/skills/analyze-agent-overlap/SKILL.md new file mode 100644 index 0000000..e49096b --- /dev/null +++ b/skills/analyze-agent-overlap/SKILL.md @@ -0,0 +1,198 @@ +--- +name: analyze-agent-overlap +description: Analyzes existing agents, skills, prompts, and instructions to identify overlaps, redundancies, and conflicts. Works with GitHub Copilot, Claude Code, Codex, OpenCode, and other providers. Use before creating new customization files to avoid duplication, when consolidating agents, or when troubleshooting conflicting behaviors. +--- + +# Analyze Agent Overlap + +Detects redundancy, overlap, and potential conflicts between AI coding assistant customization files. + +## Provider Folder Reference + +This skill works across multiple AI coding assistant providers: + +| Provider | Base Folder | +|----------|-------------| +| GitHub Copilot | `.github/` | +| Claude Code | `.claude/` | +| Codex | `.codex/` | +| OpenCode | `.config/opencode/` | + +**Throughout this document, `/` represents your chosen provider's base folder.** + +## When to Use + +- Before creating a new agent, skill, prompt, or instruction +- When you suspect two agents are doing similar things +- To audit and consolidate your customization files +- When agent behaviors seem to conflict + +## Analysis Process + +### Step 1: Inventory Existing Items + +Scan these locations (replace `/` with actual folder): +- `/agents/*.md` - All agent definitions (including `.subagent.agent.md`) +- `/skills/*/SKILL.md` - All skill definitions +- `/prompts/*.prompt.md` - All prompt templates +- `/instructions/*.instructions.md` - All instruction files + +For each item, extract: +- **Name**: The identifier +- **Purpose**: What problem it solves (from description) +- **Domain**: What areas/topics it covers +- **Triggers**: Keywords or scenarios that activate it +- **User-Invokable**: Whether it's a user-facing agent or sub-agent + +### Step 2: Compare Against Proposed Item + +When analyzing a proposed new item, compare: + +**Direct Overlap Indicators:** +- Same or very similar name +- Same primary purpose statement +- Identical target domain +- Overlapping trigger keywords (>50% match) + +**Partial Overlap Indicators:** +- Related but distinct purposes +- Some shared expertise areas +- Similar but different trigger scenarios +- Complementary functionality + +**No Overlap Indicators:** +- Different domains entirely +- Non-overlapping use cases +- Distinct trigger keywords + +### Step 3: Detect Conflicts + +Look for these conflict types: + +**Behavioral Conflicts:** +- Two agents giving contradictory guidance for same scenario +- Instructions that override each other for same file patterns +- Skills that produce incompatible outputs + +**Scope Conflicts:** +- Multiple agents claiming the same use cases +- Overlapping `applyTo` patterns in instructions +- Ambiguous routing between similar agents + +**Naming Conflicts:** +- Names too similar causing confusion +- Same name in different contexts + +## Overlap Severity Levels + +### 🔴 Critical (Do Not Proceed) +- Exact duplicate of existing item +- Direct contradiction with existing guidance +- Name collision +- >80% purpose overlap + +### 🟡 Warning (Needs Discussion) +- Significant overlap (50-80% shared purpose) +- Potential user confusion about which to use +- Overlapping triggers with different behaviors +- Partial scope conflict + +### 🟢 Low Risk (Proceed with Awareness) +- Minor overlap (<50% shared concerns) +- Complementary purposes +- Clear differentiation possible +- Different trigger contexts + +## Resolution Strategies + +When overlap is detected, consider: + +### Merge +Combine into single, more comprehensive item. +- Best when: Items serve nearly identical purpose +- Action: Create unified item, deprecate duplicates + +### Extend +Add new functionality to existing item. +- Best when: New need is subset of existing item's scope +- Action: Modify existing item, don't create new + +### Differentiate +Clarify boundaries between items. +- Best when: Items serve related but distinct needs +- Action: Update descriptions to make distinctions clear + +### Reference +Have one item delegate to another. +- Best when: Items have hierarchical relationship +- Action: Add handoff or reference in description + +### Supersede +Replace older item with improved version. +- Best when: New item is strictly better +- Action: Create new, mark old as deprecated + +## Output Format + +```markdown +## Overlap Analysis: [Proposed Item Name] + +### Summary +**Proposed Type:** [Agent|Skill|Prompt|Instruction] +**Proposed Purpose:** [Brief description] +**Overlap Level:** None | Low | Medium | High | Critical +**Recommendation:** Proceed | Modify | Merge | Reconsider + +### Comparison Matrix + +| Existing Item | Type | Overlap | Shared Concerns | +|---------------|------|---------|-----------------| +| [name] | [type] | [level] | [what overlaps] | + +### Detailed Findings + +#### High/Critical Overlap Items +[For each significant overlap:] + +**[Existing Item Name]** +- Type: [type] +- Purpose: [their purpose] +- Overlap Areas: [specific shared concerns] +- Key Distinction: [how proposed differs] +- Resolution: [recommended action] + +#### Potential Conflicts +[List any behavioral or scope conflicts] + +#### Complementary Items +[Items that could work well alongside proposed] + +### Recommendations + +1. [Primary recommendation with rationale] +2. [Secondary options if applicable] + +### Questions to Resolve +- [Clarifying questions that would help decision] +``` + +## Example Analysis + +**Proposed:** `database-helper` agent for SQL query assistance + +**Findings:** +- `dx12-terrain-engine-dev` - No overlap (different domain) +- `agent-builder` - No overlap (different domain) + +**Result:** ✅ Proceed - no conflicts detected + +--- + +**Proposed:** `code-reviewer` agent for code review + +**Findings:** +- Existing `dx12-terrain-engine-dev` mentions code quality +- Partial overlap in "review code" scenarios + +**Result:** ⚠️ Warning - clarify scope boundaries +- Recommendation: `code-reviewer` for general review, `dx12-terrain-engine-dev` for DX12-specific review only diff --git a/skills/copilot-file-specs/SKILL.md b/skills/copilot-file-specs/SKILL.md new file mode 100644 index 0000000..cd18518 --- /dev/null +++ b/skills/copilot-file-specs/SKILL.md @@ -0,0 +1,476 @@ +--- +name: copilot-file-specs +description: Contains the complete specifications for AI coding assistant customization files including agents, skills, prompts, and instructions. Works with GitHub Copilot, Claude Code, Codex, OpenCode, and other providers. Use this skill when you need to reference the correct file format, required fields, supported attributes, or file locations for any customization file. +--- + +# AI Coding Assistant Customization File Specifications + +This skill contains the authoritative specifications for all AI coding assistant customization file types. These specifications work across multiple providers. + +## Provider Folder Mapping + +Different AI coding assistant providers use similar folder structures in their respective configuration directories: + +| Provider | Base Folder | Notes | +|----------|-------------|-------| +| GitHub Copilot | `.github/` | Most common, widely documented | +| Claude Code | `.claude/` | Anthropic's Claude in VS Code | +| Codex | `.codex/` | OpenAI Codex-based tools | +| OpenCode | `.config/opencode/` | Open-source alternatives | + +**Throughout this document, `/` represents your chosen provider's base folder.** Replace with the appropriate directory for your environment. + +## File Types Overview + +| Type | Extension | Location | Purpose | +|------|-----------|----------|---------| +| Agent | `.agent.md` or `.md` | `/agents/` | Custom AI personas with specialized behaviors | +| Sub-Agent | `.subagent.agent.md` | `/agents/` | Workflow component agents (not user-invokable) | +| Skill | `SKILL.md` | `/skills//` | Reusable capabilities (directory-based) | +| Prompt | `.prompt.md` | `/prompts/` | Reusable prompt templates | +| Instruction | `.instructions.md` | `/instructions/` | Contextual guidance for file types | + +--- + +## Agent Files (`.agent.md`) + +### Location +- Workspace: `/agents/*.agent.md` or `/agents/*.md` +- Sub-agents: `/agents/*.subagent.agent.md` +- User profile: Available across workspaces + +### File Structure +```markdown +--- +name: agent-name +description: Brief description shown as placeholder in chat input +user-invokable: true +argument-hint: Optional hint for user input +tools: ['tool1', 'tool2'] +agents: ['*'] # or specific agent names, or [] for none +model: Claude Sonnet 4 # or array for fallback: ['Claude Sonnet 4', 'GPT-4o'] +disable-model-invocation: false +handoffs: + - label: Button Text + agent: target-agent + prompt: Prompt to send + send: false + model: GPT-5 (copilot) +--- + +[Agent instructions body - Markdown content] +``` + +### Frontmatter Attributes + +| Attribute | Required | Description | +|-----------|----------|-------------| +| `name` | No | Agent name. If not specified, filename is used | +| `description` | Yes (recommended) | Brief description shown as placeholder text in chat input field | +| `user-invokable` | No | Set to `false` for sub-agents that shouldn't appear in agent picker (default: `true`) | +| `argument-hint` | No | Hint text shown in chat input to guide users | +| `tools` | No | List of tool/tool set names available to this agent. Use `/*` for all MCP server tools | +| `agents` | No | List of agent names available as subagents. Use `*` for all, `[]` for none. Requires `agent` tool in tools list | +| `model` | No | AI model to use. Can be a string or array (prioritized fallback list). If not specified, uses currently selected model | +| `disable-model-invocation` | No | Set to `true` to prevent this agent from being invoked as a subagent by other agents (default: `false`) | +| `infer` | No | **Deprecated.** Use `user-invokable` and `disable-model-invocation` instead | +| `target` | No | Target environment: `vscode` or `github-copilot` | +| `mcp-servers` | No | MCP server configs for GitHub Copilot target | +| `handoffs` | No | List of handoff configurations for workflow transitions | + +### Naming Conventions + +- **User-facing agents:** `.agent.md` or `.md` +- **Sub-agents (workflow components):** `.subagent.agent.md` with `user-invokable: false` + +### Handoff Configuration + +```yaml +handoffs: + - label: "Display text for button" + agent: "target-agent-name" + prompt: "Prompt text to send to target agent" + send: false # true to auto-submit, false to pre-fill only + model: "GPT-5 (copilot)" # Optional: model for this handoff +``` + +| Attribute | Required | Description | +|-----------|----------|-------------| +| `label` | Yes | Display text shown on the handoff button | +| `agent` | Yes | Target agent identifier to switch to | +| `prompt` | No | Prompt text to send to the target agent | +| `send` | No | Auto-submit the prompt if `true` (default: `false`) | +| `model` | No | Language model for the handoff. Use format `Model Name (vendor)`, e.g., `GPT-5 (copilot)` | + +### Body Content +- Markdown formatted instructions +- Reference other files with Markdown links +- Reference tools with `#tool:` syntax +- Prepended to user chat prompt when agent is selected + +### Example +```markdown +--- +name: planner +description: Generate an implementation plan for new features +tools: ['fetch', 'githubRepo', 'search', 'usages'] +model: Claude Sonnet 4 +handoffs: + - label: Implement Plan + agent: agent + prompt: Implement the plan outlined above. + send: false +--- + +# Planning Instructions + +You are in planning mode. Generate implementation plans without making code edits. + +## Plan Structure +- Overview: Brief description +- Requirements: List of requirements +- Implementation Steps: Detailed steps +- Testing: Required tests +``` + +### Sub-Agent Example +```markdown +--- +name: due-diligence +user-invokable: false +description: Deep analysis of requirements and integration points +tools: ['search', 'fetch', 'usages'] +--- + +# Due Diligence Analysis + +You perform deep analysis on requirements before planning begins. +Identify integration points, dependencies, risks, and clarifications needed. +``` + +--- + +## Skill Files (SKILL.md) + +### Location +- Workspace: `/skills//SKILL.md` +- Each skill is a **directory** containing at minimum a `SKILL.md` file + +### Directory Structure +``` +skill-name/ +├── SKILL.md # Required - skill definition +├── scripts/ # Optional - executable code +├── references/ # Optional - additional documentation +└── assets/ # Optional - static resources (templates, images, data) +``` + +### File Structure +```markdown +--- +name: skill-name +description: What this skill does and when to use it. +license: Apache-2.0 +compatibility: Requires specific tools or environment +metadata: + author: org-name + version: "1.0" +allowed-tools: Bash(git:*) Read +--- + +[Skill instructions body - Markdown content] +``` + +### Frontmatter Attributes + +| Attribute | Required | Constraints | +|-----------|----------|-------------| +| `name` | Yes | 1-64 chars, lowercase alphanumeric + hyphens, must match directory name | +| `description` | Yes | 1-1024 chars, describes function and trigger keywords | +| `license` | No | License name or reference to bundled file | +| `compatibility` | No | 1-500 chars, environment requirements | +| `metadata` | No | Key-value pairs for additional info | +| `allowed-tools` | No | Space-delimited pre-approved tools (experimental) | + +### Name Validation Rules +- Must be 1-64 characters +- Lowercase letters, numbers, and hyphens only (`a-z`, `0-9`, `-`) +- Cannot start or end with hyphen +- Cannot contain consecutive hyphens (`--`) +- **Must match parent directory name exactly** + +### Body Content +- Markdown formatted instructions +- No format restrictions +- Recommended sections: + - Step-by-step instructions + - Examples of inputs and outputs + - Common edge cases +- Keep under 500 lines; split longer content into reference files + +### Progressive Disclosure +1. **Metadata** (~100 tokens): `name` and `description` loaded at startup +2. **Instructions** (<5000 tokens recommended): Full body loaded when activated +3. **Resources** (as needed): Files in subdirectories loaded on demand + +### Example +```markdown +--- +name: code-review +description: Performs thorough code review focusing on security, performance, and best practices. Use when reviewing pull requests, checking code quality, or identifying potential issues. +--- + +# Code Review Skill + +Analyzes code for quality, security, and adherence to best practices. + +## Review Process + +1. Check for security vulnerabilities +2. Identify performance issues +3. Verify coding standards compliance +4. Suggest improvements + +## Output Format + +Provide findings organized by severity: Critical, Warning, Info. +``` + +--- + +## Prompt Files (`.prompt.md`) + +### Location +- Workspace: `/prompts/*.prompt.md` +- User profile: Available across workspaces + +### File Structure +```markdown +--- +name: prompt-name +description: What this prompt accomplishes +argument-hint: Guide for user input +agent: agent-name +model: Claude Sonnet 4 +tools: ['tool1', 'tool2'] +--- + +[Prompt template body with variables] +``` + +### Frontmatter Attributes + +| Attribute | Required | Description | +|-----------|----------|-------------| +| `name` | No | Prompt name used after `/` in chat. Defaults to filename | +| `description` | No | Short description of the prompt | +| `argument-hint` | No | Hint text for user input guidance | +| `agent` | No | Agent to use: `ask`, `edit`, `agent`, or custom agent name | +| `model` | No | Language model to use. Defaults to selected model | +| `tools` | No | List of available tools for this prompt | + +### Body Content +- Markdown formatted prompt template +- Reference workspace files with relative Markdown links +- Reference tools with `#tool:` syntax + +### Variables + +| Variable Type | Syntax | Examples | +|---------------|--------|----------| +| Workspace | `${workspaceFolder}`, `${workspaceFolderBasename}` | Project paths | +| Selection | `${selection}`, `${selectedText}` | Editor selection | +| File Context | `${file}`, `${fileBasename}`, `${fileDirname}`, `${fileBasenameNoExtension}` | Current file info | +| Input | `${input:varName}`, `${input:varName:placeholder}` | User-provided values | + +### Example +```markdown +--- +name: create-react-form +description: Generate a React form component with validation +agent: agent +tools: ['editFiles'] +--- + +# Create React Form Component + +Generate a React form component named ${input:formName:MyForm} with the following requirements: + +- Use TypeScript +- Include form validation +- Follow project conventions in [coding standards](../instructions/react.instructions.md) + +## Form Fields +${input:fields:Describe the form fields needed} +``` + +--- + +## Instruction Files (`.instructions.md`) + +### Location +- Workspace: `/instructions/*.instructions.md` +- User profile: Available across workspaces + +### File Structure +```markdown +--- +name: Friendly Name +description: What these instructions cover +applyTo: "**/*.ts" +--- + +[Instruction content - Markdown] +``` + +### Frontmatter Attributes + +| Attribute | Required | Description | +|-----------|----------|-------------| +| `name` | No | Display name in UI. Defaults to filename | +| `description` | No | Short description of the instructions | +| `applyTo` | No* | Glob pattern(s) for automatic application | + +*If `applyTo` is not specified, instructions won't apply automatically but can be manually attached. + +### ApplyTo Patterns +- Single pattern: `"**/*.ts"` +- Multiple patterns: `["**/*.ts", "**/*.tsx"]` +- Use `**` to apply to all files +- Patterns are relative to workspace root +- Applied when creating/modifying files (not read operations) + +### Body Content +- Markdown formatted guidelines +- Reference tools with `#tool:` syntax +- Reference other files with Markdown links + +### Example +```markdown +--- +name: Python Standards +description: Coding standards for Python files +applyTo: "**/*.py" +--- + +# Python Coding Standards + +- Follow PEP 8 style guide +- Always include type hints +- Write docstrings for all public functions +- Use 4 spaces for indentation +- Prefer f-strings over .format() or % formatting +``` + +--- + +## Other Instruction Types + +### Global Instructions (`/copilot-instructions.md`) +- Single file at provider folder root +- Applies to ALL chat requests automatically +- Enable with `github.copilot.chat.codeGeneration.useInstructionFiles` setting (for GitHub Copilot) +- Also recognized by GitHub Copilot in Visual Studio and GitHub.com + +### AGENTS.md +- Place at workspace root +- Applies to all chat requests +- Useful for multi-agent workspaces +- Enable with `chat.useAgentsMdFile` setting +- Nested `AGENTS.md` files supported (experimental) with `chat.useNestedAgentsMdFiles` +- When nested files enabled, VS Code searches recursively in subfolders + +### Organization-Level Instructions +- Share instructions across multiple workspaces and repositories within a GitHub organization +- Defined at the GitHub organization level +- Enable with `github.copilot.chat.organizationInstructions.enabled` setting +- Automatically detected and shown alongside personal/workspace instructions + +### Instruction Settings for Specific Scenarios + +You can configure custom instructions for specialized scenarios via VS Code settings: + +| Setting | Purpose | +|---------|---------|| +| `github.copilot.chat.reviewSelection.instructions` | Code review instructions | +| `github.copilot.chat.commitMessageGeneration.instructions` | Commit message generation | +| `github.copilot.chat.pullRequestDescriptionGeneration.instructions` | PR title/description generation | + +**Format:** Array of objects with `text` (inline) or `file` (reference) property: +```json +{ + "github.copilot.chat.reviewSelection.instructions": [ + { "text": "Always check for security vulnerabilities." }, + { "file": "guidance/review-guidelines.md" } + ] +} +``` + +--- + +## Tool Reference Syntax + +In any body content, reference tools using: +``` +#tool: +``` + +Example: "Use #tool:githubRepo to access repository information." + +--- + +## File Location Summary + +``` +/ # .github/, .claude/, .codex/, .config/opencode/, etc. +├── copilot-instructions.md # Global instructions (single file) +├── agents/ +│ ├── my-agent.agent.md # User-facing agent +│ ├── another-agent.md # User-facing agent (also valid) +│ └── helper.subagent.agent.md # Sub-agent (not user-invokable) +├── skills/ +│ └── my-skill/ +│ ├── SKILL.md # Required skill definition +│ ├── scripts/ # Optional executable code +│ ├── references/ # Optional documentation +│ └── assets/ # Optional static resources +├── prompts/ +│ └── my-prompt.prompt.md # Prompt template +└── instructions/ + └── python.instructions.md # Contextual instructions +``` + +--- + +## VS Code Settings Reference + +### Core Settings + +| Setting | Purpose | +|---------|---------|| +| `github.copilot.chat.codeGeneration.useInstructionFiles` | Enable `/copilot-instructions.md` | +| `chat.instructionsFilesLocations` | Additional instruction file folders | +| `chat.promptFilesLocations` | Additional prompt file folders | +| `chat.agentFilesLocations` | Additional agent file folders | +| `chat.useAgentsMdFile` | Enable `AGENTS.md` file | +| `chat.useNestedAgentsMdFiles` | Enable nested `AGENTS.md` files | +| `chat.useAgentSkills` | Enable skills in `.claude/skills/` or `.github/skills/` | + +### Instruction Behavior Settings + +| Setting | Purpose | +|---------|---------|| +| `chat.includeApplyingInstructions` | Enable instructions with `applyTo` patterns | +| `chat.includeReferencedInstructions` | Enable instructions referenced via Markdown links | +| `github.copilot.chat.organizationInstructions.enabled` | Enable organization-level instructions | +| `github.copilot.chat.organizationCustomAgents.enabled` | Enable organization-level custom agents | + +--- + +## Tips for Defining Custom Instructions + +- Keep instructions short and self-contained - each should be a single, simple statement +- For task or language-specific instructions, use multiple `.instructions.md` files with selective `applyTo` patterns +- Store project-specific instructions in your workspace to share with team members +- Reuse and reference instructions files in prompt files and custom agents to avoid duplication +- Instructions are applied when creating/modifying files, typically not for read operations diff --git a/skills/curseforge-maven/SKILL.md b/skills/curseforge-maven/SKILL.md new file mode 100644 index 0000000..c7d2781 --- /dev/null +++ b/skills/curseforge-maven/SKILL.md @@ -0,0 +1,71 @@ +--- +name: curseforge-maven +description: Add CurseForge mod dependencies from a CurseForge mod URL to Maven builds (Hytale). Uses latest release, adds the CurseForge Maven repo, and supports CURSE_USER/CURSE_TOKEN auth. Triggers: curseforge url, add mod, maven dependency, curseforge maven. +compatibility: Requires Maven and CurseForge credentials via CURSE_USER and CURSE_TOKEN env vars. +metadata: + version: "1.0" +--- + +# CurseForge Maven Dependency Skill + +Add a CurseForge mod (by URL) as a Maven dependency using the CurseForge Maven endpoint. + +## Inputs +- **CurseForge mod URL** (required) +- **Latest file name** (optional but often required): the newest release .jar filename from the project’s Files list + +## Requirements +- Environment variables: + - `CURSE_USER` = your CurseForge account email + - `CURSE_TOKEN` = your CurseForge API token +- Maven repository: `https://www.curseforge.com/api/maven/` + +> Note: CurseForge’s docs indicate Maven auth is embedded in the URL, but in practice some endpoints require API token auth. This skill uses `CURSE_USER`/`CURSE_TOKEN` via Maven server credentials and can fall back to a `?token=` query parameter if needed. + +## Project Setup (Maven) +1. Ensure the CurseForge repository exists in [pom.xml](pom.xml): + - `id`: `curseforge` + - `url`: `https://www.curseforge.com/api/maven/` +2. Ensure Maven uses credentials via [.mvn/settings.xml](.mvn/settings.xml): + - `username`: `${env.CURSE_USER}` + - `password`: `${env.CURSE_TOKEN}` +3. If you still receive 401 errors, update the repository URL to include the token: + - `https://www.curseforge.com/api/maven/?token=${env.CURSE_TOKEN}` + +## How to Derive Maven Coordinates (Latest Release) +Given a mod URL like: +``` +https://www.curseforge.com/hytale/mods/ +``` + +1. **projectSlug** = last path segment (example: `my-mod`). +2. Get the **latest release filename** from the mod’s Files list, e.g. + - `MyMod-1.2.3-release-universal.jar` +3. Parse the filename: + - `mavenArtifact` = `MyMod-1.2.3` + - `mavenVersion` = `release` + - `projectFileNameTag` (classifier) = `universal` + +Maven dependency format: +```xml + + projectSlug + mavenArtifact + release + projectFileNameTag + +``` + +## Scripted Add (Recommended) +Use the PowerShell script in: +- [.github/skills/curseforge-maven/scripts/add-curseforge-mod.ps1](.github/skills/curseforge-maven/scripts/add-curseforge-mod.ps1) + +It will: +- Extract the slug from the URL +- Ask for (or parse) the latest file name +- Add the dependency and repository to the Maven `pom.xml` + +## Edge Cases +- If the filename doesn’t include a tag (e.g., no `-universal`), omit the classifier. +- If `release` doesn’t resolve, use the actual file version from the name instead (e.g., `1.2.3`). +- If the mod is not a `.jar`, Maven dependency may not work. diff --git a/skills/curseforge-maven/scripts/add-curseforge-mod.ps1 b/skills/curseforge-maven/scripts/add-curseforge-mod.ps1 new file mode 100644 index 0000000..a2ce998 --- /dev/null +++ b/skills/curseforge-maven/scripts/add-curseforge-mod.ps1 @@ -0,0 +1,158 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ModUrl, + + [string]$PomPath = "pom.xml", + + [string]$RepositoryUrl = "https://www.curseforge.com/api/maven/", + + [string]$ReleaseType = "release", + + [string]$FileName +) + +function Get-CurseForgeSlug { + param([string]$Url) + + try { + $uri = [System.Uri]$Url + $path = $uri.AbsolutePath.TrimEnd('/') + $segments = $path.Split('/') | Where-Object { $_ -ne "" } + if (-not $segments -or $segments.Count -lt 1) { + throw "URL path not recognized." + } + return $segments[-1] + } catch { + throw "Invalid URL: $Url" + } +} + +function Parse-FileName { + param( + [string]$Name, + [string]$ReleaseType + ) + + $clean = $Name.Trim() + if ($clean.EndsWith(".jar")) { + $clean = $clean.Substring(0, $clean.Length - 4) + } + + $marker = "-$ReleaseType-" + if ($clean -like "*$marker*") { + $parts = $clean -split [regex]::Escape($marker), 2 + return [pscustomobject]@{ + Artifact = $parts[0] + Tag = $parts[1] + } + } + + $suffix = "-$ReleaseType" + if ($clean.EndsWith($suffix)) { + $artifact = $clean.Substring(0, $clean.Length - $suffix.Length) + return [pscustomobject]@{ + Artifact = $artifact + Tag = "" + } + } + + return $null +} + +function New-Element { + param( + [xml]$Document, + [string]$Name, + [string]$NamespaceUri, + [string]$Value + ) + + $element = $Document.CreateElement($Name, $NamespaceUri) + if ($null -ne $Value) { + $element.InnerText = $Value + } + return $element +} + +if (-not (Test-Path $PomPath)) { + throw "pom.xml not found at path: $PomPath" +} + +$slug = Get-CurseForgeSlug -Url $ModUrl + +if (-not $FileName) { + $FileName = Read-Host "Enter latest release file name (e.g., MyMod-1.2.3-release-universal.jar)" +} + +$parsed = Parse-FileName -Name $FileName -ReleaseType $ReleaseType +if (-not $parsed) { + Write-Warning "Could not parse the file name." + $artifact = Read-Host "Enter mavenArtifact (file name without -$ReleaseType-)" + $tag = Read-Host "Enter file tag/classifier (e.g., universal) or leave blank" +} else { + $artifact = $parsed.Artifact + $tag = $parsed.Tag +} + +[xml]$pom = Get-Content $PomPath +$nsUri = $pom.Project.NamespaceURI +$ns = New-Object System.Xml.XmlNamespaceManager($pom.NameTable) +$ns.AddNamespace("m", $nsUri) + +$projectNode = $pom.SelectSingleNode("/m:project", $ns) +if (-not $projectNode) { + throw "Invalid pom.xml: missing root." +} + +$repositoriesNode = $pom.SelectSingleNode("/m:project/m:repositories", $ns) +if (-not $repositoriesNode) { + $repositoriesNode = New-Element -Document $pom -Name "repositories" -NamespaceUri $nsUri + [void]$projectNode.AppendChild($repositoriesNode) +} + +$repoNode = $pom.SelectSingleNode("/m:project/m:repositories/m:repository[m:id='curseforge']", $ns) +if (-not $repoNode) { + $repoNode = New-Element -Document $pom -Name "repository" -NamespaceUri $nsUri + [void]$repositoriesNode.AppendChild($repoNode) + [void]$repoNode.AppendChild((New-Element -Document $pom -Name "id" -NamespaceUri $nsUri -Value "curseforge")) + [void]$repoNode.AppendChild((New-Element -Document $pom -Name "name" -NamespaceUri $nsUri -Value "CurseForge Maven")) + [void]$repoNode.AppendChild((New-Element -Document $pom -Name "url" -NamespaceUri $nsUri -Value $RepositoryUrl)) +} else { + $urlNode = $repoNode.SelectSingleNode("m:url", $ns) + if ($urlNode) { + $urlNode.InnerText = $RepositoryUrl + } else { + [void]$repoNode.AppendChild((New-Element -Document $pom -Name "url" -NamespaceUri $nsUri -Value $RepositoryUrl)) + } +} + +$dependenciesNode = $pom.SelectSingleNode("/m:project/m:dependencies", $ns) +if (-not $dependenciesNode) { + $dependenciesNode = New-Element -Document $pom -Name "dependencies" -NamespaceUri $nsUri + [void]$projectNode.AppendChild($dependenciesNode) +} + +$dependencyQuery = "/m:project/m:dependencies/m:dependency[m:groupId='$slug' and m:artifactId='$artifact' and m:version='$ReleaseType']" +if ($tag -and $tag.Trim() -ne "") { + $dependencyQuery += " and m:classifier='$tag'" +} + +$dependencyNode = $pom.SelectSingleNode($dependencyQuery, $ns) +if ($dependencyNode) { + Write-Host "Dependency already exists: $slug:$artifact:$ReleaseType" -ForegroundColor Yellow +} else { + $dependencyNode = New-Element -Document $pom -Name "dependency" -NamespaceUri $nsUri + [void]$dependencyNode.AppendChild((New-Element -Document $pom -Name "groupId" -NamespaceUri $nsUri -Value $slug)) + [void]$dependencyNode.AppendChild((New-Element -Document $pom -Name "artifactId" -NamespaceUri $nsUri -Value $artifact)) + [void]$dependencyNode.AppendChild((New-Element -Document $pom -Name "version" -NamespaceUri $nsUri -Value $ReleaseType)) + if ($tag -and $tag.Trim() -ne "") { + [void]$dependencyNode.AppendChild((New-Element -Document $pom -Name "classifier" -NamespaceUri $nsUri -Value $tag)) + } + [void]$dependenciesNode.AppendChild($dependencyNode) + Write-Host "Added dependency: $slug:$artifact:$ReleaseType" -ForegroundColor Green +} + +$pom.Save($PomPath) +Write-Host "Updated pom.xml at $PomPath" -ForegroundColor Green +Write-Host "Ensure CURSE_USER and CURSE_TOKEN are set in your environment." -ForegroundColor Cyan diff --git a/skills/generate-agent-docs/SKILL.md b/skills/generate-agent-docs/SKILL.md new file mode 100644 index 0000000..9695a89 --- /dev/null +++ b/skills/generate-agent-docs/SKILL.md @@ -0,0 +1,266 @@ +--- +name: generate-agent-docs +description: Generates documentation and usage guides for agents, skills, prompts, and instructions. Works with GitHub Copilot, Claude Code, Codex, OpenCode, and other providers. Use when onboarding team members, creating README files for your customizations, or generating usage examples for existing agents. +--- + +# Generate Agent Documentation + +Creates user-friendly documentation for AI coding assistant customization files. + +## Provider Folder Reference + +This skill works across multiple AI coding assistant providers: + +| Provider | Base Folder | +|----------|-------------| +| GitHub Copilot | `.github/` | +| Claude Code | `.claude/` | +| Codex | `.codex/` | +| OpenCode | `.config/opencode/` | + +**Throughout this document, `/` represents your chosen provider's base folder.** + +## When to Use + +- Onboarding new team members to your agent ecosystem +- Creating a catalog of available agents and skills +- Generating usage examples for specific agents +- Documenting your customization setup for reference + +## Documentation Types + +### Individual Item Documentation + +Generate detailed docs for a single agent, skill, prompt, or instruction. + +### Catalog Documentation + +Generate a comprehensive overview of all customization files. + +## Output Templates + +### Agent Documentation + +```markdown +# Agent: [Agent Name] + +## Overview +[What this agent does and why it exists] + +## When to Use This Agent + +Use `@[agent-name]` when: +- [Scenario 1] +- [Scenario 2] +- [Scenario 3] + +**Don't use** when: +- [Anti-pattern 1] +- [Anti-pattern 2] + +## How It Behaves + +This agent will: +- [Behavior 1] +- [Behavior 2] +- [Behavior 3] + +## Example Conversations + +### Example 1: [Scenario Title] + +**You:** [Example user message] + +**Agent:** [How agent responds - summarized] + +### Example 2: [Scenario Title] + +**You:** [Example user message] + +**Agent:** [How agent responds - summarized] + +## Tips for Best Results + +- [Tip 1 for effective usage] +- [Tip 2 for effective usage] +- [Common mistake to avoid] + +## Related + +- **[Related Agent]**: Use for [distinction] +- **[Related Skill]**: This agent uses this for [purpose] +``` + +### Skill Documentation + +```markdown +# Skill: [Skill Name] + +## Purpose +[What this skill accomplishes] + +## Triggers +This skill activates when: +- [Trigger keyword/scenario 1] +- [Trigger keyword/scenario 2] + +## What It Does +[Step-by-step of what the skill does] + +## Used By +- [Agent 1] - for [purpose] +- [Agent 2] - for [purpose] + +## Example + +**Scenario:** [Description] + +**Input:** [What's provided] + +**Output:** [What's produced] +``` + +### Prompt Documentation + +```markdown +# Prompt: [Prompt Name] + +## Purpose +[What task this prompt accomplishes] + +## Mode +`[mode]` - [explanation of what this mode does] + +## Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `{{var1}}` | [what it's for] | [example value] | + +## How to Use + +1. [Step 1] +2. [Step 2] + +## Example + +**With these values:** +- `{{var1}}` = [value] + +**Produces:** +[Example output] +``` + +### Instruction Documentation + +```markdown +# Instructions: [Name] + +## Applies To +Files matching: `[glob pattern]` + +## Purpose +[What guidance these instructions provide] + +## Key Rules + +1. [Rule 1] +2. [Rule 2] +3. [Rule 3] + +## When Active +These instructions automatically apply when you're working with files that match the pattern above. + +## Examples of Affected Files +- `[example path 1]` +- `[example path 2]` +``` + +### Catalog Documentation + +```markdown +# AI Coding Assistant Customizations + +This document catalogs all custom agents, skills, prompts, and instructions configured for this project. + +## Provider +[Specify which provider folder is used: `.github/`, `.claude/`, `.codex/`, `.config/opencode/`] + +## Quick Reference + +### Agents (User-Invokable) + +| Agent | Purpose | Invoke With | +|-------|---------|-------------| +| [name] | [brief purpose] | `@[name]` | + +### Sub-Agents (Workflow Components) + +| Sub-Agent | Purpose | Used By | +|-----------|---------|---------| +| [name] | [brief purpose] | [parent workflow agent] | + +### Skills + +| Skill | Purpose | Triggers | +|-------|---------|----------| +| [name] | [brief purpose] | [keywords] | + +### Prompts + +| Prompt | Mode | Purpose | +|--------|------|---------| +| [name] | [mode] | [brief purpose] | + +### Instructions + +| Instructions | Applies To | Purpose | +|--------------|------------|---------| +| [name] | [pattern] | [brief purpose] | + +## Detailed Documentation + +[Full documentation for each item] + +## Usage Guidelines + +[General guidance on how to use these customizations effectively] +``` + +## Generation Process + +### Step 1: Read Source File +Load the agent/skill/prompt/instruction file. + +### Step 2: Extract Key Information +- Name and description from frontmatter +- Behaviors and rules from body +- Examples if present +- Related items (skills, handoffs) + +### Step 3: Enhance with Context +- Generate additional examples based on purpose +- Identify related items from the ecosystem +- Add tips based on common patterns + +### Step 4: Format Output +Apply appropriate template based on item type. + +## Quality Guidelines + +Generated documentation should be: + +1. **Clear** - No jargon without explanation +2. **Practical** - Real, actionable examples +3. **Complete** - Covers all key aspects +4. **Concise** - No unnecessary padding +5. **Current** - Reflects actual file contents + +## Additional Examples + +When generating examples beyond those in the source: + +- Cover different use case variations +- Show edge cases and how they're handled +- Demonstrate integration with other items +- Illustrate common mistakes to avoid diff --git a/skills/hytale-blocks/SKILL.md b/skills/hytale-blocks/SKILL.md new file mode 100644 index 0000000..18a2107 --- /dev/null +++ b/skills/hytale-blocks/SKILL.md @@ -0,0 +1,197 @@ +--- +name: hytale-blocks +description: Documents how to create custom blocks in Hytale plugins using asset packs and JSON definitions. Use when creating blocks, defining block JSON, configuring block textures, materials, gathering, block types, or setting up block asset folder structure. Triggers - block, create block, custom block, BlockType, block JSON, block definition, block texture, block material, DrawType, Gathering, block creation, asset pack, IncludesAssetPack, block item, Cube block, block sound, block particle. +--- + +# Hytale Custom Blocks + +Reference for creating custom blocks in Hytale plugins via asset packs and JSON item definitions with `BlockType` configuration. + +> **Source:** +> **Related skills:** For block *components* and ECS ticking behavior, see `hytale-ecs`. For items and interactions, see `hytale-items`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Enable asset packs | Set `"IncludesAssetPack": true` in `manifest.json` | +| Define a block | Create `Server/Item/Items/.json` with a `BlockType` section | +| Set block texture | `"Textures": [{ "All": "BlockTextures/.png" }]` | +| Set block material | `"Material": "Solid"` (or `Liquid`, `NonSolid`, etc.) | +| Set draw type | `"DrawType": "Cube"` (or `Cross`, `Slab`, etc.) | +| Add localized name | `Server/Languages/en-US/items.lang` → `.name = Display Name` | +| Set gathering/breaking | `"Gathering": { "Breaking": { "GatherType": "...", "ItemId": "..." } }` | +| Set block icon | `"Icon": "Icons/ItemsGenerated/.png"` | + +--- + +## Prerequisites + +### Enable Asset Packs + +Your plugin's `manifest.json` must declare asset pack inclusion: + +```json +{ + "IncludesAssetPack": true, + "dependencies": ["Hytale:EntityModule", "Hytale:BlockModule"] +} +``` + +### Folder Structure + +``` +src/main/resources/ +├── manifest.json +├── Server/ +│ ├── Item/ +│ │ └── Items/ +│ │ └── my_new_block.json # Block definition +│ └── Languages/ +│ └── en-US/ +│ └── items.lang # Translations +└── Common/ + ├── Icons/ # Item icons + ├── Blocks/ + │ └── my_new_block/ + │ └── model.blockymodel # Block model + └── BlockTextures/ + └── my_new_block.png # Block texture +``` + +--- + +## Translations + +Create `Server/Languages/en-US/items.lang`: + +``` +my_new_block.name = My New Block +my_new_block.description = My Description +``` + +> The filename `items` becomes the translation key prefix, so `"items.my_new_block.name"` resolves to `My New Block`. + +--- + +## Block JSON Definition + +Create `Server/Item/Items/my_new_block.json`: + +```json +{ + "TranslationProperties": { + "Name": "items.my_new_block.name", + "Description": "items.my_new_block.description" + }, + "Id": "My_New_Block", + "MaxStack": 100, + "Icon": "Icons/ItemsGenerated/my_new_block.png", + "Categories": [ + "Blocks.Rocks" + ], + "PlayerAnimationsId": "Block", + "Set": "Rock_Stone", + "BlockType": { + "Material": "Solid", + "DrawType": "Cube", + "Group": "Stone", + "Flags": {}, + "Gathering": { + "Breaking": { + "GatherType": "Rocks", + "ItemId": "my_new_block" + } + }, + "BlockParticleSetId": "Stone", + "Textures": [ + { + "All": "BlockTextures/my_new_block.png" + } + ], + "ParticleColor": "#aeae8c", + "BlockSoundSetId": "Stone", + "BlockBreakingDecalId": "Breaking_Decals_Rock" + }, + "ResourceTypes": [ + { + "Id": "Rock" + } + ] +} +``` + +--- + +## BlockType Properties + +| Property | Description | Examples | +|----------|-------------|---------| +| `Material` | Physics material type | `"Solid"`, `"Liquid"`, `"NonSolid"` | +| `DrawType` | How the block is rendered | `"Cube"`, `"Cross"`, `"Slab"` | +| `Group` | Block category group | `"Stone"`, `"Wood"`, `"Sand"` | +| `Flags` | Additional block flags | `{}` (empty object for defaults) | +| `Gathering.Breaking.GatherType` | Tool type needed to break | `"Rocks"`, `"Wood"`, `"Sand"` | +| `Gathering.Breaking.ItemId` | Item dropped when broken | ID string matching the block's `Id` | +| `BlockParticleSetId` | Particle effect when breaking | `"Stone"`, `"Wood"`, `"Sand"` | +| `Textures` | Array of texture definitions | See Texture Configuration below | +| `ParticleColor` | Break particle color | Hex color string `"#aeae8c"` | +| `BlockSoundSetId` | Sound set for interactions | `"Stone"`, `"Wood"`, `"Sand"` | +| `BlockBreakingDecalId` | Breaking animation decal | `"Breaking_Decals_Rock"` | + +### Texture Configuration + +Textures are defined as an array of objects. Use `"All"` to apply one texture to all faces, or specify per-face: + +```json +"Textures": [ + { + "All": "BlockTextures/my_block.png" + } +] +``` + +Per-face texturing (when supported): + +```json +"Textures": [ + { + "Top": "BlockTextures/my_block_top.png", + "Bottom": "BlockTextures/my_block_bottom.png", + "Side": "BlockTextures/my_block_side.png" + } +] +``` + +--- + +## Item Properties (Top-Level) + +These properties are standard item fields that the block also uses: + +| Property | Description | +|----------|-------------| +| `TranslationProperties` | `Name` and `Description` translation keys | +| `Id` | Unique identifier for the item/block | +| `MaxStack` | Maximum stack size in inventory | +| `Icon` | Path to inventory icon image | +| `Categories` | Array of category tags (e.g., `"Blocks.Rocks"`) | +| `PlayerAnimationsId` | Animation set when held (e.g., `"Block"`) | +| `Set` | Visual set grouping (e.g., `"Rock_Stone"`) | +| `ResourceTypes` | Array of resource type objects with `Id` field | + +--- + +## Edge Cases & Gotchas + +- All referenced files (textures, models, icons) must exist at the specified paths or the block will fail to load +- The `Id` field is case-sensitive and must be unique across all items and blocks +- Translation keys follow the pattern `..name` — the `.lang` filename is the prefix +- `IncludesAssetPack` must be `true` in manifest — without it, `Common/` assets are ignored +- Block textures go in `Common/BlockTextures/`, not `Common/Textures/` +- The `ItemId` in `Gathering.Breaking` should match the block's `Id` for the block to drop itself when broken +- Check `lib/Server/` for existing block definitions to see all available property values + +``` diff --git a/skills/hytale-camera-controls/SKILL.md b/skills/hytale-camera-controls/SKILL.md new file mode 100644 index 0000000..debb3b3 --- /dev/null +++ b/skills/hytale-camera-controls/SKILL.md @@ -0,0 +1,233 @@ +--- +name: hytale-camera-controls +description: Documents Hytale's camera system for customizing player camera via ServerCameraSettings and SetServerCamera packets. Use when creating custom camera modes, top-down cameras, side-scroller cameras, isometric cameras, adjusting zoom distance, camera smoothing, rotation, cursor display, or resetting camera to default. Triggers - camera, ServerCameraSettings, SetServerCamera, ClientCameraView, camera preset, top-down, side-scroller, isometric, camera distance, camera rotation, camera zoom, first person, third person, camera controls, RotationType, MouseInputType, MovementForceRotationType. +--- + +# Hytale Camera Controls + +Use this skill when customizing the player camera in Hytale plugins. The camera is controlled server-side by sending a `SetServerCamera` packet with `ServerCameraSettings` to configure distance, rotation, input mode, movement alignment, and more. + +> **Source:** + +--- + +## Quick Reference + +| Task | How | +|------|-----| +| Set custom camera | Send `SetServerCamera(ClientCameraView.Custom, true, settings)` | +| Reset to default | Send `SetServerCamera(ClientCameraView.Custom, false, null)` | +| Lock camera | Set `isLocked = true` in the packet | +| Top-down view | See Top-Down preset below | +| Side-scroller view | See Side-Scroller preset below | +| Isometric view | See Isometric preset below | +| Prevent wall clipping | Use `PositionDistanceOffsetType.DistanceOffsetRaycast` | + +--- + +## Required Imports + +```java +import com.hypixel.hytale.protocol.ApplyLookType; +import com.hypixel.hytale.protocol.ClientCameraView; +import com.hypixel.hytale.protocol.Direction; +import com.hypixel.hytale.protocol.MouseInputType; +import com.hypixel.hytale.protocol.MovementForceRotationType; +import com.hypixel.hytale.protocol.PositionDistanceOffsetType; +import com.hypixel.hytale.protocol.RotationType; +import com.hypixel.hytale.protocol.ServerCameraSettings; +import com.hypixel.hytale.protocol.Vector3f; +import com.hypixel.hytale.protocol.packets.camera.SetServerCamera; +``` + +--- + +## The Basics + +### Applying Custom Camera Settings + +Create a `ServerCameraSettings` object, configure its fields, and send it to the player via a `SetServerCamera` packet: + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.distance = 10.0f; // Zoom distance from player +settings.isFirstPerson = false; // Third-person mode +settings.positionLerpSpeed = 0.2f; // Smooth camera follow + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +### Resetting to Default Camera + +Pass `false` and `null` to restore the default camera: + +```java +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, false, null) +); +``` + +--- + +## Camera Presets + +### Top-Down (RTS / ARPG Style) + +Source: `com.hypixel.hytale.server.core.command.commands.player.camera.PlayerCameraTopdownCommand` + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.positionLerpSpeed = 0.2f; +settings.rotationLerpSpeed = 0.2f; +settings.distance = 20.0f; +settings.displayCursor = true; +settings.isFirstPerson = false; +settings.movementForceRotationType = MovementForceRotationType.Custom; +// Align movement with camera yaw (horizontal rotation only) +settings.movementForceRotation = new Direction(-0.7853981634f, 0.0f, 0.0f); // 45° right +settings.eyeOffset = true; +settings.positionDistanceOffsetType = PositionDistanceOffsetType.DistanceOffset; +settings.rotationType = RotationType.Custom; +settings.rotation = new Direction(0.0f, -1.5707964f, 0.0f); // Look straight down +settings.mouseInputType = MouseInputType.LookAtPlane; +settings.planeNormal = new Vector3f(0.0f, 1.0f, 0.0f); // Ground plane + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +### Side-Scroller (2D Platformer Style) + +Source: `com.hypixel.hytale.server.core.command.commands.player.camera.PlayerCameraSideScrollerCommand` + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.positionLerpSpeed = 0.2f; +settings.rotationLerpSpeed = 0.2f; +settings.distance = 15.0f; +settings.displayCursor = true; +settings.isFirstPerson = false; +settings.movementForceRotationType = MovementForceRotationType.Custom; +settings.movementMultiplier = new Vector3f(1.0f, 1.0f, 0.0f); // Lock Z-axis +settings.eyeOffset = true; +settings.positionDistanceOffsetType = PositionDistanceOffsetType.DistanceOffset; +settings.rotationType = RotationType.Custom; +settings.mouseInputType = MouseInputType.LookAtPlane; +settings.planeNormal = new Vector3f(0.0f, 0.0f, 1.0f); // Side plane + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +### Isometric Character Camera (Diablo Style) + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.positionLerpSpeed = 0.2f; +settings.rotationLerpSpeed = 0.2f; +settings.isFirstPerson = false; +settings.distance = 6f; +settings.allowPitchControls = false; +settings.displayCursor = true; + +// Force the camera's rotation to be set by the server +settings.applyLookType = ApplyLookType.Rotation; + +// Notify that we provide a custom rotation +settings.rotationType = RotationType.Custom; + +// Set the typical isometric rotation +Direction rotation = new Direction( + (float) Math.toRadians(45f), // yaw + (float) Math.toRadians(-35f), // pitch + 0f // roll +); +settings.rotation = rotation; +settings.movementForceRotation = rotation; + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +--- + +## Common Settings Explained + +### Position & Rotation + +| Setting | Type | Description | +|---------|------|-------------| +| `positionLerpSpeed` | `float` (0.0–1.0) | How smoothly the camera follows the player. Lower = smoother but slower. | +| `rotationLerpSpeed` | `float` (0.0–1.0) | How smoothly the camera rotates. Lower = smoother but slower. | +| `distance` | `float` | Camera distance from the player. Higher = more zoomed out. | +| `rotation` | `Direction(yaw, pitch, roll)` | Camera angle in **radians**. | +| `rotationType` | `RotationType` | How rotation is calculated. Use `RotationType.Custom` for your own `rotation` value. | + +### Movement Alignment + +| Setting | Type | Description | +|---------|------|-------------| +| `movementForceRotationType` | `MovementForceRotationType` | `AttachedToHead` = movement follows where player looks; `Custom` = use `movementForceRotation`. | +| `movementForceRotation` | `Direction` | When using `Custom`, sets the direction for W/S movement. Match yaw with camera but keep pitch at 0. | +| `movementMultiplier` | `Vector3f` | Scale movement on each axis. E.g., `(1,1,0)` locks Z-axis for 2D movement. | + +### Input & Display + +| Setting | Type | Description | +|---------|------|-------------| +| `displayCursor` | `boolean` | Show or hide the mouse cursor. | +| `mouseInputType` | `MouseInputType` | `LookAtPlane` = mouse moves cursor on a plane (top-down); `LookAtTarget` = mouse rotates camera. | +| `planeNormal` | `Vector3f` | For `LookAtPlane`, defines which plane the mouse moves on. `(0,1,0)` = ground plane. | + +### Advanced + +| Setting | Type | Description | +|---------|------|-------------| +| `positionDistanceOffsetType` | `PositionDistanceOffsetType` | `DistanceOffset` = simple offset; `DistanceOffsetRaycast` = prevents camera clipping through walls. | +| `eyeOffset` | `boolean` | Offset camera from the player's eye position. | +| `isFirstPerson` | `boolean` | `true` for first-person, `false` for third-person. | +| `allowPitchControls` | `boolean` | Allow player to adjust pitch. Set `false` to lock vertical angle. | +| `applyLookType` | `ApplyLookType` | `ApplyLookType.Rotation` forces the camera rotation to be server-controlled. | + +--- + +## Tips + +- **Zoom:** Adjust `distance` (higher = further out). +- **Smoothness:** `positionLerpSpeed` and `rotationLerpSpeed` control how quickly the camera catches up to its target. +- **Wall clipping:** Use `PositionDistanceOffsetType.DistanceOffsetRaycast` to prevent the camera from going through walls. +- **Lock camera:** Set `isLocked = true` in the packet to prevent player changes. +- **2D movement:** Set `movementMultiplier` to zero out an axis (e.g., `new Vector3f(1, 1, 0)` for side-scroller). +- **Isometric cameras:** Always set `movementForceRotation` to match camera yaw for proper movement alignment. +- **Angle calculations:** Use `Math.toRadians(degrees)` to convert degrees to radians. All rotation values use radians. +- **Movement alignment:** When using `MovementForceRotationType.Custom`, match the yaw from your camera rotation but keep pitch at `0` so movement stays on the horizontal plane. + +--- + +## Enums Reference + +### ClientCameraView +- `ClientCameraView.Custom` — Required view type for applying custom camera settings. + +### RotationType +- `RotationType.Custom` — Use the `rotation` value from settings. + +### MovementForceRotationType +- `MovementForceRotationType.AttachedToHead` — Movement follows where the player looks. +- `MovementForceRotationType.Custom` — Use `movementForceRotation` to define movement direction. + +### MouseInputType +- `MouseInputType.LookAtPlane` — Mouse moves cursor on a defined plane (good for top-down/isometric). +- `MouseInputType.LookAtTarget` — Mouse rotates the camera around the player. + +### PositionDistanceOffsetType +- `PositionDistanceOffsetType.DistanceOffset` — Simple distance offset from player. +- `PositionDistanceOffsetType.DistanceOffsetRaycast` — Distance offset with raycast to prevent wall clipping. + +### ApplyLookType +- `ApplyLookType.Rotation` — Server controls the camera rotation. diff --git a/skills/hytale-chat-formatting/SKILL.md b/skills/hytale-chat-formatting/SKILL.md new file mode 100644 index 0000000..f81fe85 --- /dev/null +++ b/skills/hytale-chat-formatting/SKILL.md @@ -0,0 +1,349 @@ +--- +name: hytale-chat-formatting +description: Formats chat messages in Hytale plugins using PlayerChatEvent and TinyMessage rich text. Use when creating chat formatters, applying colors/gradients, adding clickable links, styling messages, or handling chat events. Triggers - chat, message, PlayerChatEvent, TinyMsg, gradient, chat color, rich text, sendMessage, Formatter. +--- + +# Hytale Chat Formatting Skill + +Use this skill when working on chat message formatting in Hytale plugins. This covers the `PlayerChatEvent`, manual `Message` formatting, and the TinyMessage rich text library. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **PlayerChatEvent** | Event triggered when a player sends a chat message | +| **Formatter** | Interface to customize how chat messages are displayed | +| **Message** | Hytale's message object for styled text | +| **TinyMsg** | Third-party library for rich text parsing (gradients, colors, links) | + +--- + +## PlayerChatEvent + +The `PlayerChatEvent` is triggered when a player sends a chat message. You can: +- Cancel the event to block the message +- Modify the content +- Set a custom formatter +- Access the sender and target list + +### Event Properties + +| Method | Description | +|--------|-------------| +| `getSender()` | Returns the `PlayerRef` who sent the message | +| `getContent()` | Returns the message content as a String | +| `setContent(String)` | Modify the message content | +| `setCancelled(boolean)` | Cancel the event to block the message | +| `setFormatter(Formatter)` | Set a custom formatter for the message | +| `getTargets()` | List of players who will see the message | + +--- + +## Manual Formatting (Standard Approach) + +Use `Message` API directly for basic formatting without external dependencies. + +### Basic Example + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.Color; +import com.hypixel.hytale.server.event.player.PlayerChatEvent; +import com.hypixel.hytale.server.player.PlayerRef; + +public class ChatFormatter { + public static void onPlayerChat(PlayerChatEvent event) { + PlayerRef sender = event.getSender(); + + // Block specific words + if (event.getContent().equalsIgnoreCase("poo")) { + event.setCancelled(true); + sender.sendMessage(Message.raw("Hey, you cannot say that!").color(Color.RED)); + return; + } + + // Modify message content + if (event.getContent().equalsIgnoreCase("you stink")) { + event.setContent("i stink"); + } + + // Set custom formatter + event.setFormatter((playerRef, message) -> + Message.join( + Message.raw("[COOL] ").color(Color.RED), + Message.raw(sender.getUsername()).color(Color.YELLOW), + Message.raw(" : " + message).color(Color.PINK) + )); + } +} +``` + +### Formatter Interface + +```java +public interface Formatter { + @Nonnull + Message format(@Nonnull PlayerRef playerRef, @Nonnull String message); +} +``` + +### Message API Methods + +| Method | Description | +|--------|-------------| +| `Message.raw(String)` | Create a message from raw text | +| `Message.join(Message...)` | Join multiple messages together | +| `.color(Color)` | Apply a color to the message | + +### Available Colors + +Standard `Color` class provides: `RED`, `YELLOW`, `PINK`, `BLUE`, `GREEN`, `WHITE`, `BLACK`, `GOLD`, `GRAY`, `AQUA`, etc. + +--- + +## TinyMessage - Rich Text Library + +TinyMessage is a lightweight rich text parser for Hytale servers, similar to Minecraft's MiniMessage. It provides an easier way to create styled messages with gradients, hex colors, and clickable links. + +### Features + +- **Gradients**: Multi-color text gradients +- **Hex Colors**: Custom colors using hex codes +- **Standard Styles**: Bold, italic, underline, monospace +- **Clickable Links**: URL links in chat +- **Nested Styling**: Combine multiple styles + +--- + +## TinyMessage Tags Reference + +### Color Tags + +| Tag | Aliases | Example | Description | +|-----|---------|---------|-------------| +| `` | ``, `` | `text` | Named or hex color | +| `` | `` | `text` | Color gradient | + +### Style Tags + +| Tag | Aliases | Example | Description | +|-----|---------|---------|-------------| +| `` | `` | `text` | Bold text | +| `` | ``, `` | `text` | Italic text | +| `` | `` | `text` | Underlined text | +| `` | `` | `text` | Monospace font | +| `` | `` | `boldnormal` | Reset all formatting | + +### Link Tags + +| Tag | Aliases | Example | Description | +|-----|---------|---------|-------------| +| `` | `` | `click` | Clickable link | + +### Named Colors + +`black`, `dark_blue`, `dark_green`, `dark_aqua`, `dark_red`, `dark_purple`, `gold`, `gray`, `dark_gray`, `blue`, `green`, `aqua`, `red`, `light_purple`, `yellow`, `white` + +--- + +## TinyMessage Usage Examples + +### Basic Usage + +```java +import fi.sulku.hytale.TinyMsg; +import com.hypixel.hytale.server.core.Message; + +// Parse a formatted string into a Message +Message message = TinyMsg.parse("Hello World!"); +player.sendMessage(message); + +// Multiple styles +TinyMsg.parse("Bold Gold Text"); + +// Clickable gradient link +TinyMsg.parse("Click me!"); + +// Complex nested styling +TinyMsg.parse("Bold and italic and red"); + +// Reset styles mid-text +TinyMsg.parse("Bold normal text"); +``` + +### Chat Event with TinyMessage + +```java +public class ChatFormatter { + private void onPlayerChat(PlayerChatEvent event) { + PlayerRef sender = event.getSender(); + + if (event.getContent().equalsIgnoreCase("poo")) { + event.setCancelled(true); + sender.sendMessage(TinyMsg.parse("Hey, you cannot say that!")); + return; + } + + if (event.getContent().equalsIgnoreCase("you stink")) { + event.setContent("i stink"); + } + + // Custom chat format with TinyMessage + event.setFormatter((playerRef, message) -> + TinyMsg.parse("[COOL] " + sender.getUsername() + + " : " + message + "")); + } +} +``` + +### Gradient Examples + +```java +// Two-color gradient +TinyMsg.parse("Rainbow text!"); + +// Multi-color gradient +TinyMsg.parse("Fire gradient!"); + +// Gradient with hex colors +TinyMsg.parse("Custom gradient"); +``` + +### Hex Color Examples + +```java +// Hex color +TinyMsg.parse("Custom purple"); + +// Named color +TinyMsg.parse("Gold text"); + +// Short form +TinyMsg.parse("Short syntax"); +``` + +--- + +## TinyMessage Installation + +### For Server Owners + +Download `TinyMessage.jar` from releases and place in server's `mods` folder. + +### For Developers + +#### manifest.json + +Add the dependency to your manifest: + +```json +"Dependencies": { + "Zoltus:TinyMessage": "*" +} +``` + +#### Maven (pom.xml) + +```xml + + + jitpack.io + https://jitpack.io + + + + + com.github.Zoltus + TinyMessage + 2.0.1 + provided + +``` + +#### Gradle (build.gradle) + +```groovy +repositories { + maven { url = uri("https://jitpack.io") } +} + +dependencies { + compileOnly("com.github.Zoltus:TinyMessage:2.0.1") +} +``` + +--- + +## API Reference + +### TinyMsg.parse(String text) + +Parses a string with TinyMsg tags and returns a `Message` object. + +**Parameters:** +- `text` - The string containing TinyMessage tags + +**Returns:** +- `Message` - A Hytale `Message` object ready to be sent to players + +--- + +## Common Patterns + +### Rank Prefix Chat Format + +```java +event.setFormatter((playerRef, message) -> + TinyMsg.parse("[ADMIN] " + + playerRef.getUsername() + ": " + message + "")); +``` + +### Colored Server Announcements + +```java +public void announce(String text) { + Message announcement = TinyMsg.parse( + "[SERVER] " + text + ""); + // Send to all players +} +``` + +### Private Message Format + +```java +public void sendPrivateMessage(PlayerRef from, PlayerRef to, String message) { + Message formatted = TinyMsg.parse( + "[PM] " + + "" + from.getUsername() + " → " + to.getUsername() + + ": " + message + ""); + to.sendMessage(formatted); +} +``` + +--- + +## Choosing an Approach + +| Use Case | Recommended Approach | +|----------|---------------------| +| Simple color formatting | Manual `Message` API | +| Complex gradients/styling | TinyMessage | +| No external dependencies | Manual `Message` API | +| Rapid development | TinyMessage | +| Clickable links in chat | TinyMessage | + +--- + +## Resources + +- [HytaleModding Chat Formatting Guide](https://hytalemodding.dev/en/docs/guides/plugin/chat-formatting) +- [TinyMessage GitHub Repository](https://github.com/Zoltus/TinyMessage/) +- [TinyMessage Releases](https://github.com/Zoltus/TinyMessage/releases) + +--- + +## License + +TinyMessage is available under the MIT License. diff --git a/skills/hytale-commands/SKILL.md b/skills/hytale-commands/SKILL.md new file mode 100644 index 0000000..dc20e1c --- /dev/null +++ b/skills/hytale-commands/SKILL.md @@ -0,0 +1,463 @@ +--- +name: hytale-commands +description: Documents Hytale's command system for creating custom commands in plugins. Covers AbstractAsyncCommand, AbstractPlayerCommand, AbstractTargetPlayerCommand, AbstractTargetEntityCommand, AbstractCommandCollection, arguments (RequiredArg, OptionalArg, DefaultArg, FlagArg), ArgTypes, argument validators, custom validators, permissions, command variants, aliases, subcommands, and registration. Use when creating commands, adding arguments, validating input, requiring permissions, building command trees, or registering commands. Triggers - command, custom command, AbstractPlayerCommand, AbstractAsyncCommand, AbstractTargetPlayerCommand, AbstractTargetEntityCommand, AbstractCommandCollection, CommandContext, RequiredArg, OptionalArg, DefaultArg, FlagArg, ArgTypes, Validator, requirePermission, addUsageVariant, addAliases, addSubCommand, registerCommand, CommandRegistry. +--- + +# Hytale Command System + +Comprehensive reference for creating custom commands in Hytale plugins, including command types, arguments, validators, permissions, variants, subcommands, and registration. + +> **Source:** +> **Related skills:** For permissions in detail, see `hytale-permissions`. For player stats used in commands, see `hytale-player-stats`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Basic async command | Extend `AbstractAsyncCommand`, override `executeAsync()` | +| Player-bound command | Extend `AbstractPlayerCommand`, override `execute()` | +| Target another player | Extend `AbstractTargetPlayerCommand` (adds `--player` arg) | +| Target looked-at entity | Extend `AbstractTargetEntityCommand` (uses raycast) | +| Add required argument | `this.withRequiredArg("name", "desc", ArgTypes.STRING)` | +| Add optional argument | `this.withOptionalArg("name", "desc", ArgTypes.STRING)` | +| Add default argument | `this.withDefaultArg("name", "desc", ArgTypes.FLOAT, 100f, "default desc")` | +| Add flag argument | `this.withFlagArg("name", "desc")` | +| Get argument value | `myArg.get(commandContext)` | +| Require permission | `requirePermission(HytalePermissions.fromCommand("name"))` | +| Make command public | Override `canGeneratePermission()` to return `false` | +| Add variant | `addUsageVariant(new OtherCommand())` | +| Add alias | `addAliases("alias1", "alias2")` | +| Group subcommands | Extend `AbstractCommandCollection`, call `addSubCommand(...)` | +| Register command | `getCommandRegistry().registerCommand(new MyCommand())` in `setup()` | + +--- + +## Command Types + +### AbstractAsyncCommand + +Runs on a background thread. Cannot safely access `Store` or `Ref` without getting the world first. Best for world-independent commands (e.g., displaying rules). + +```java +public class ServerRulesCommand extends AbstractAsyncCommand { + + public ServerRulesCommand() { + super("rules", "Lists the servers rules"); + } + + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext context) { + context.sendMessage(Message.raw("The only rule is there are no rules.")); + return CompletableFuture.completedFuture(null); + } +} +``` + +> **Warning:** `AbstractAsyncCommand` runs asynchronously - it cannot edit Stores or Refs without first getting the desired world. For most commands, prefer the other command types. + +### AbstractPlayerCommand + +Tied to the executing player and their world. Runs on the world thread - safe to access `Store` and `Ref` directly. Most common command type. + +```java +public class ExampleCommand extends AbstractPlayerCommand { + + public ExampleCommand() { + super("test", "Super test command!"); + } + + @Override + protected void execute(@Nonnull CommandContext commandContext, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + Player player = store.getComponent(ref, Player.getComponentType()); + UUIDComponent component = store.getComponent(ref, UUIDComponent.getComponentType()); + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + player.sendMessage(Message.raw("Transform : " + transform.getPosition())); + } +} +``` + +> **Note:** Long-running operations (like IO) in `AbstractPlayerCommand` will block the world thread and cause lag. Use `AbstractAsyncCommand` for heavy IO. + +### AbstractTargetPlayerCommand + +Like `AbstractPlayerCommand` but adds a `--player ` argument to target a different player. Thread-safe - override `execute()`, not `executeAsync()`. + +### AbstractTargetEntityCommand + +Uses a raycast to target the entity the player is looking at. Runs on the world thread of the targeted entity. + +```java +@Override +protected void execute(CommandContext context, + Store store, + Ref ref, + World world) { + // ref is the targeted entity's reference + EntityStatMap stats = store.getComponent(ref, EntityStatMap.getComponentType()); + if (stats == null) { + context.sendMessage(Message.raw("This entity has no stats!")); + return; + } + int healthIdx = DefaultEntityStatTypes.getHealth(); + EntityStatValue health = stats.get(healthIdx); + if (health == null) { + context.sendMessage(Message.raw("This entity has no health!")); + return; + } + stats.addValue(healthIdx, 100); +} +``` + +--- + +## Arguments + +Arguments are added in the command's constructor. The value is retrieved during `execute` by passing `commandContext` to the argument's `get()` method. + +### Argument Types + +| Method | Behavior | Usage | +|--------|----------|-------| +| `withRequiredArg(name, desc, type)` | Must be provided; parsed left-to-right positionally | `RequiredArg` | +| `withOptionalArg(name, desc, type)` | Returns `null` if not provided; uses `--key value` syntax | `OptionalArg` | +| `withDefaultArg(name, desc, type, default, defaultDesc)` | Returns default if not provided | `DefaultArg` | +| `withFlagArg(name, desc)` | Boolean switch; `true` if present, `false` if not; uses `--name` | `FlagArg` | + +### ArgTypes + +Common argument types: + +- `ArgTypes.STRING` +- `ArgTypes.INTEGER` +- `ArgTypes.BOOLEAN` +- `ArgTypes.FLOAT` +- `ArgTypes.DOUBLE` +- `ArgTypes.UUID` +- `ArgTypes.PLAYER_REF` + +### Full Arguments Example + +```java +// Usage: /healplayer --health 50 --message "Feels Good" --debug +public class HealPlayerCommand extends AbstractTargetPlayerCommand { + private final DefaultArg healthArg; + private final OptionalArg messageArg; + private final FlagArg debugArg; + + public HealPlayerCommand() { + super("healplayer", "Healing a player for an amount of HP (default: 100)"); + + this.healthArg = this.withDefaultArg("health", "Amount to heal player", + ArgTypes.FLOAT, (float) 100, "Desc of Default: 100"); + this.messageArg = this.withOptionalArg("message", + "Message to print while healing", ArgTypes.STRING); + this.debugArg = this.withFlagArg("debug", "Add debug logs"); + } + + @Override + protected void execute(@Nonnull CommandContext commandContext, + @Nullable Ref ref, + @Nonnull Ref ref1, + @Nonnull PlayerRef playerRef, + @Nonnull World world, + @Nonnull Store store) { + + if (this.debugArg.get(commandContext)) { + commandContext.sendMessage(Message.raw("We are debugging")); + } + + EntityStatMap stats = store.getComponent(ref, EntityStatMap.getComponentType()); + int healthIdx = DefaultEntityStatTypes.getHealth(); + stats.addStatValue(healthIdx, healthArg.get(commandContext)); + } +} +``` + +--- + +## Argument Validators + +Add validators to arguments using `.addValidator()`. Built-in validators are in the `Validators` class: + +```java +OptionalArg healAmount = withOptionalArg("amount", "Heal Amount", ArgTypes.INTEGER) + .addValidator(Validators.greaterThan(0)) + .addValidator(Validators.lessThan(1000)); +``` + +### Custom Validators + +Implement `com.hypixel.hytale.codec.validation.Validator`: + +```java +import com.hypixel.hytale.codec.schema.SchemaContext; +import com.hypixel.hytale.codec.schema.config.Schema; +import com.hypixel.hytale.codec.validation.ValidationResults; +import com.hypixel.hytale.codec.validation.Validator; + +public class MyCustomValidator implements Validator { + @Nonnull + private final String bannedValue; + + public MyCustomValidator(@Nonnull String bannedValue) { + this.bannedValue = bannedValue; + } + + @Override + public void accept(@Nullable String input, @Nonnull ValidationResults results) { + if (this.bannedValue.equalsIgnoreCase(input)) { + results.fail("The given value has been banned."); + } + } + + @Override + public void updateSchema(SchemaContext context, @Nonnull Schema target) { + // Optional: update schema for dynamic validation + throw new UnsupportedOperationException("Not implemented yet."); + } +} +``` + +Usage: + +```java +String bannedRole = "badword"; +OptionalArg roleArg = withOptionalArg("role", "Role to assign", ArgTypes.STRING) + .addValidator(new MyCustomValidator(bannedRole)); +``` + +--- + +## Permissions + +Add permission requirements in the constructor: + +```java +public HealPlayerCommand() { + super("healplayer", "heal a player a given amount of HP"); + + // Single permission + requirePermission(HytalePermissions.fromCommand("rules")); + + // Multiple required permissions (AND) + requirePermission(HytalePermissions.fromCommand("usercommands")); + + // OR block - needs one from a list + requirePermission( + PermissionRules.or( + HytalePermissions.fromCommand("moderator"), + HytalePermissions.fromCommand("admin") + ) + ); +} +``` + +> Use `/perm` in-game to manage player permissions and groups. Run `/perm --help` for usage. + +### Making a Command Require No Permission + +**Override `canGeneratePermission()` (RECOMMENDED):** +```java +@Override +protected boolean canGeneratePermission() { + return false; // Prevents auto-generated permission +} +``` + +For subcommands, **BOTH parent AND child** must return `false`: + +```java +public class ParentCommand extends AbstractCommandCollection { + public ParentCommand() { + super("parent", "desc"); + this.addSubCommand(new ChildCommand()); + } + + @Override + protected boolean canGeneratePermission() { + return false; + } +} + +public class ChildCommand extends AbstractAsyncCommand { + public ChildCommand() { + super("child", "desc"); + } + + @Override + protected boolean canGeneratePermission() { + return false; + } +} +``` + +### Mixed Permission Model + +Parent skips permission generation; each child decides individually: + +```java +public class ParentCommand extends AbstractCommandCollection { + public ParentCommand() { + super("parent", "desc"); + this.addSubCommand(new PublicCommand()); // No permission + this.addSubCommand(new AdminCommand()); // Requires permission + } + + @Override + protected boolean canGeneratePermission() { + return false; + } +} + +public class PublicCommand extends AbstractPlayerCommand { + @Override + protected boolean canGeneratePermission() { + return false; + } +} + +public class AdminCommand extends AbstractPlayerCommand { + public AdminCommand() { + super("admin", "desc"); + this.requirePermission("myplugin.admin.command"); + } +} +``` + +--- + +## Command Variants & Aliases + +Use `addUsageVariant()` for alternate forms of the same command, and `addAliases()` for shorthand names: + +```java +public class GiveCommand extends AbstractPlayerCommand { + private final RequiredArg itemArg; + + public GiveCommand() { + super("give", "Give item to yourself"); + this.itemArg = withRequiredArg("item", "Item", ArgTypes.STRING); + addUsageVariant(new GiveOtherCommand()); + addAliases("gv", "gMe"); + } + // execute... +} + +// Variant - NOTE: no command name in super() +public static class GiveOtherCommand extends AbstractAsyncCommand { + private final RequiredArg itemArg; + private final RequiredArg playerArg; + + public GiveOtherCommand() { + super("Give item to another player"); // description only + this.playerArg = withRequiredArg("player", "Target Player", ArgTypes.PLAYER_REF); + this.itemArg = withRequiredArg("item", "Item", ArgTypes.STRING); + } +} +``` + +--- + +## Subcommands & Command Collections + +Group commands under a parent using `AbstractCommandCollection`: + +``` +/admin + |-- user + | |-- rules + | |-- teleport + |-- server + |-- restart +``` + +```java +public class UserCommandCollection extends AbstractCommandCollection { + public UserCommandCollection() { + super("user", "User commands"); + addSubCommand(new RulesCommand()); + addSubCommand(new TeleportCommand()); + } +} + +public class AdminCommand extends AbstractCommandCollection { + public AdminCommand() { + super("admin", "Admin commands"); + addSubCommand(new UserCommandCollection()); + addSubCommand(new ServerCommandCollection()); + } +} +``` + +> `AbstractCommandCollection` itself cannot execute - all logic must be in subcommands. But you can nest collections within collections. + +--- + +## Registration + +Register commands in your plugin's `setup()` method: + +```java +public class MyPlugin extends JavaPlugin { + @Override + public void setup() { + this.getCommandRegistry().registerCommand(new ExampleCommand()); + + // Or: + CommandRegistry registry = getCommandRegistry(); + registry.registerCommand(new ExampleCommand()); + } +} +``` + +--- + +## Sending Messages + +```java +// To player via PlayerRef +playerRef.sendMessage(Message.raw("Hello!")); + +// With color (use hex string, NOT int) +playerRef.sendMessage(Message.raw("Success!").color("#55FF55")); + +// Chained messages using insert() +Message msg = Message.raw("Prefix: ").color("#AAAAAA") + .insert(Message.raw("Value").color("#FFFFFF")); +playerRef.sendMessage(msg); + +// Via CommandContext (works for console and player) +context.sendMessage(Message.raw("Message")); +``` + +--- + +## Edge Cases & Gotchas + +- `AbstractAsyncCommand` runs on a background thread - cannot safely access `Store`/`Ref` without getting the world first +- `AbstractPlayerCommand` runs on the world thread - long IO operations will cause lag +- Required args are parsed left-to-right positionally; optional/default args use `--key value` syntax +- Flag args are boolean switches using `--name` syntax +- When getting argument values, always pass `commandContext`: `myArg.get(commandContext)` +- `addUsageVariant()` variants must NOT pass a command name to `super()` - only pass the description +- `canGeneratePermission()` must return `false` on BOTH parent and child for fully public subcommands +- `setPermissionGroup(null)` alone is NOT sufficient if auto-generated permissions are still active + +--- + +## Related Packages + +- `com.hypixel.hytale.server.core.command.system.AbstractCommand` +- `com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand` +- `com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncCommand` +- `com.hypixel.hytale.server.core.command.system.basecommands.AbstractTargetPlayerCommand` +- `com.hypixel.hytale.server.core.command.system.basecommands.AbstractTargetEntityCommand` +- `com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection` +- `com.hypixel.hytale.codec.validation.Validator` +- `com.hypixel.hytale.codec.validation.Validators` diff --git a/skills/hytale-config-files/SKILL.md b/skills/hytale-config-files/SKILL.md new file mode 100644 index 0000000..409f3f5 --- /dev/null +++ b/skills/hytale-config-files/SKILL.md @@ -0,0 +1,307 @@ +--- +name: hytale-config-files +description: Creates and manages plugin configuration files in Hytale using Config, BuilderCodec, and KeyedCodec for persistent settings that survive server restarts. Use when creating config classes, loading/saving plugin settings, defining serializable config options, or accessing config from other classes. Triggers - config, configuration, Config, withConfig, config file, plugin settings, plugin config, config save, config load, configuration file, server settings. +--- + +# Hytale Plugin Configuration Files + +Use this skill when creating and managing configuration files for Hytale plugins. Configuration files allow plugins to store persistent settings that survive server restarts. + +> **Source:** + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Create config class | Class with `BuilderCodec` defining serializable fields | +| Register config | `this.withConfig("Name", MyConfig.CODEC)` in plugin field initializer | +| Ensure file exists | `config.save()` in `setup()` | +| Read config values | `config.get().getFieldName()` | +| Modify config values | `config.get().setFieldName(value)` then `config.save()` | +| Access from other classes | Pass `Config` or plugin reference with a getter | + +--- + +## Key Concepts + +### Config Class + +A plain Java class that holds your configuration data. It must define a `BuilderCodec` that tells Hytale how to serialize/deserialize each field. The class does NOT need to implement `Component` — it is a standalone config object. + +### Config Wrapper + +The `Config` class (provided by Hytale) wraps your config class and manages file I/O. You obtain an instance via `JavaPlugin.withConfig()`. + +### BuilderCodec Keys Must Be Capitalized + +Keys in `KeyedCodec` must start with a **capital letter**. Lowercase keys will throw an error when loading the configuration file. + +### Config File Location + +Configuration files are stored in the `mods` folder of the server, persisting across restarts. + +--- + +## Required Imports + +```java +import com.hypixel.hytale.codec.Codec; // Careful: use this Codec, not other Codec imports +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +``` + +--- + +## Creating a Configuration Class + +Define a class with fields, a `BuilderCodec`, a default constructor, getters, and setters. + +```java +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +public class MyConfig { + + // === Codec Definition === + public static final BuilderCodec CODEC = + BuilderCodec.builder(MyConfig.class, MyConfig::new) + .append(new KeyedCodec("SomeValue", Codec.INTEGER), + (config, value) -> config.someValue = value, // setter + (config) -> config.someValue) // getter + .add() + .append(new KeyedCodec("SomeString", Codec.STRING), + (config, value) -> config.someString = value, + (config) -> config.someString) + .add() + .build(); + + // === Fields with defaults === + private int someValue = 12; + private String someString = "My default string"; + + // === Default Constructor === + public MyConfig() { + } + + // === Getters === + public int getSomeValue() { + return someValue; + } + + public String getSomeString() { + return someString; + } + + // === Setters === + public void setSomeValue(int someValue) { + this.someValue = someValue; + } + + public void setSomeString(String someString) { + this.someString = someString; + } +} +``` + +### Key Rules + +- **Codec keys must be capitalized** — `"SomeValue"` not `"someValue"`. +- Each field needs a getter lambda and a setter lambda in the codec chain. +- Default field values are used when the config file doesn't yet exist. +- The config class does **not** need `clone()` or `Component` — that's only for ECS components. + +--- + +## Loading, Saving, and Using the Configuration + +### Registering the Config in Your Plugin + +Register the config as a **field initializer** in your `JavaPlugin` class using `this.withConfig()`. The config must be loaded before `setup()` completes — loading it after will throw an error. + +Call `config.save()` in `setup()` to ensure the file is created on first run. + +```java +import com.hypixel.hytale.server.plugin.JavaPlugin; +import com.hypixel.hytale.server.plugin.JavaPluginInit; +import com.hypixel.hytale.server.plugin.config.Config; + +import javax.annotation.Nonnull; + +public class ExamplePlugin extends JavaPlugin { + + // Register config — MUST be in the field initializer, not in setup() + private final Config config = this.withConfig("MyConfig", MyConfig.CODEC); + + public ExamplePlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + // Ensures the config file is created if it doesn't exist + config.save(); + } + + // Getter for other classes to access + public Config getConfig() { + return config; + } +} +``` + +### Important Timing + +| When | What | +|------|------| +| Field initialization | Call `this.withConfig(...)` to register the config | +| `setup()` | Call `config.save()` to write defaults if file missing | +| After `setup()` | **Too late** — loading config here throws an error | + +--- + +### Accessing Config from Other Classes + +Pass the plugin instance or the `Config` directly to other classes. + +```java +public class SomeOtherClass { + + private final ExamplePlugin plugin; + + public SomeOtherClass(ExamplePlugin plugin) { + this.plugin = plugin; + } + + public void someMethod() { + // Read values + MyConfig myConfig = plugin.getConfig().get(); + int value = myConfig.getSomeValue(); + String str = myConfig.getSomeString(); + + // Modify values + myConfig.setSomeValue(999); + myConfig.setSomeString("A new string"); + + // Persist changes to disk + plugin.getConfig().save(); + } +} +``` + +--- + +## Common Codec Types for Config Fields + +| Java Type | Codec | Example Key | +|-----------|-------|-------------| +| `int` | `Codec.INTEGER` | `"MaxPlayers"` | +| `long` | `Codec.LONG` | `"CooldownMs"` | +| `float` | `Codec.FLOAT` | `"SpeedMultiplier"` | +| `double` | `Codec.DOUBLE` | `"SpawnRadius"` | +| `boolean` | `Codec.BOOLEAN` | `"Enabled"` | +| `String` | `Codec.STRING` | `"WelcomeMessage"` | + +For complex types (maps, lists, sets), see the `hytale-persistent-data` skill which documents `MapCodec`, `ListCodec`, and `SetCodec`. + +--- + +## Config vs Persistent Data + +| | Config (`Config`) | Persistent Data (`Component`) | +|---|---|---| +| **Purpose** | Plugin-wide settings | Per-entity/per-player data | +| **Storage** | File in `mods/` folder | BSON on entity store | +| **Registration** | `this.withConfig(...)` | `getEntityStoreRegistry().registerComponent(...)` | +| **Requires** | `BuilderCodec` only | `BuilderCodec`, `Component`, `clone()` | +| **Access** | `config.get()` | `store.getComponent(ref, type)` | +| **When to use** | Server settings, feature toggles, thresholds | Player progress, entity state, session data | + +--- + +## Complete Example + +A full plugin with a configuration file for a welcome message and max player setting: + +```java +// === WelcomeConfig.java === +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +public class WelcomeConfig { + + public static final BuilderCodec CODEC = + BuilderCodec.builder(WelcomeConfig.class, WelcomeConfig::new) + .append(new KeyedCodec("WelcomeMessage", Codec.STRING), + (c, v) -> c.welcomeMessage = v, + c -> c.welcomeMessage) + .add() + .append(new KeyedCodec("EnableWelcome", Codec.BOOLEAN), + (c, v) -> c.enableWelcome = v, + c -> c.enableWelcome) + .add() + .append(new KeyedCodec("MaxWarnings", Codec.INTEGER), + (c, v) -> c.maxWarnings = v, + c -> c.maxWarnings) + .add() + .build(); + + private String welcomeMessage = "Welcome to the server!"; + private boolean enableWelcome = true; + private int maxWarnings = 3; + + public WelcomeConfig() {} + + public String getWelcomeMessage() { return welcomeMessage; } + public void setWelcomeMessage(String msg) { this.welcomeMessage = msg; } + + public boolean isEnableWelcome() { return enableWelcome; } + public void setEnableWelcome(boolean enable) { this.enableWelcome = enable; } + + public int getMaxWarnings() { return maxWarnings; } + public void setMaxWarnings(int max) { this.maxWarnings = max; } +} +``` + +```java +// === WelcomePlugin.java === +import com.hypixel.hytale.server.plugin.JavaPlugin; +import com.hypixel.hytale.server.plugin.JavaPluginInit; +import com.hypixel.hytale.server.plugin.config.Config; + +import javax.annotation.Nonnull; + +public class WelcomePlugin extends JavaPlugin { + + private final Config config = this.withConfig("WelcomeConfig", WelcomeConfig.CODEC); + + public WelcomePlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + config.save(); // Create file with defaults if it doesn't exist + } + + public Config getWelcomeConfig() { + return config; + } +} +``` + +--- + +## Troubleshooting + +| Problem | Cause | Fix | +|---------|-------|-----| +| Error when loading config | Codec key not capitalized | Change `"someValue"` → `"SomeValue"` | +| Error: config loaded too late | `withConfig()` called in `setup()` or later | Move to field initializer | +| Config file not created | `config.save()` not called in `setup()` | Add `config.save()` to `setup()` | +| Changes not persisted | Forgot to call `save()` after modification | Call `config.save()` after changing values | +| Wrong `Codec` import | Using a different library's `Codec` class | Use `com.hypixel.hytale.codec.Codec` | +``` diff --git a/skills/hytale-ecs/SKILL.md b/skills/hytale-ecs/SKILL.md new file mode 100644 index 0000000..0ba9d94 --- /dev/null +++ b/skills/hytale-ecs/SKILL.md @@ -0,0 +1,631 @@ +--- +name: hytale-ecs +description: Core Hytale ECS (Entity Component System) architecture and patterns for plugin development. Covers Store, EntityStore, ChunkStore, Holder, Ref, Components, Systems (EntityTickingSystem, TickingSystem, DelayedEntitySystem, RefChangeSystem), Queries, SystemGroups, CommandBuffer, block components, and plugin registration. Use when creating components, systems, queries, or working with entity/block data. Triggers - ECS, entity component system, Store, EntityStore, ChunkStore, Holder, Ref, Component, System, Query, CommandBuffer, SystemGroup, ArchetypeChunk, ComponentType, registerComponent, registerSystem, block component, block tick, RefChangeSystem, EntityTickingSystem, TickingSystem, DelayedEntitySystem. +--- + +# Hytale ECS (Entity Component System) + +Comprehensive reference for Hytale's ECS architecture. This is the foundation of all plugin development. + +> **Related skills:** For Codec/BuilderCodec serialization details, see `hytale-persistent-data`. For entity effects using ECS, see `hytale-entity-effects`. + +## Quick Reference + +| Task | Approach | +|------|----------| +| Access entity data | `store.getComponent(ref, ComponentType)` | +| Queue component change | `commandBuffer.addComponent(ref, componentType, instance)` | +| Build new entity | Create `Holder`, add components, call `store.addEntity(holder, reason)` | +| Get entity handle | `archetypeChunk.getReferenceTo(index)` returns `Ref` | +| Per-entity tick logic | Extend `EntityTickingSystem` | +| Global tick logic | Extend `TickingSystem` | +| Interval-based logic | Extend `DelayedEntitySystem` | +| React to component changes | Extend `RefChangeSystem` | +| Filter entities | `Query.and(componentTypes...)`, `Query.not(componentType)` | +| Register component | `getEntityStoreRegistry().registerComponent(Class, factory)` in `setup()` | +| Register system | `getEntityStoreRegistry().registerSystem(system)` in `start()` | +| Register block component | `getChunkStoreRegistry().registerComponent(Class, name, CODEC)` in `setup()` | +| Register block system | `getChunkStoreRegistry().registerSystem(system)` in `start()` | + +--- + +## Core Architecture + +ECS follows **composition over inheritance**: Entities are identifiers, Components are pure data, Systems contain logic. + +### Store + +The `Store` class is the core of Hytale's ECS. It stores entities using **archetypes** — entities with the same set of components are chunked together for fast retrieval. + +``` +Store +├── EntityStore — entities in a World (players, mobs, NPCs, projectiles) +└── ChunkStore — block data in a World (chunks, block sections, block components) +``` + +### EntityStore + +`EntityStore` extends `Store` and implements `WorldProvider`, giving access to a specific Hytale `World`. It maintains internal lookups: + +- `entitiesByUuid` — find entity by persistent UUID +- `networkIdToRef` — find entity by networking ID + +Every entity has a `UUIDComponent` and `NetworkId` for these lookups. + +### ChunkStore + +`ChunkStore` manages block/chunk components. Contains `WorldChunk` components (which hold `EntityChunk` for entities in the chunk and `BlockChunk` with `BlockSection`s). Use for block systems and ticking blocks. + +### Holder (Entity Blueprint) + +A `Holder` is a staging cart / blueprint for an entity. Collect all components, then "check out" at the Store: + +```java +// Conceptual flow (see Universe.addPlayer for real example): +// 1. Create Holder and add components +// 2. store.addEntity(holder, AddReason.LOAD) → returns Ref +``` + +`PlayerStorage#load` returns `CompletableFuture>` — async loading that eventually adds to the store. + +### Ref (Reference Handle) + +A **safe handle/pointer** to an entity. **Never store direct references to entity objects** — use `Ref` instead. + +```java +Ref ref = archetypeChunk.getReferenceTo(index); + +// Validate before use (throws if entity deleted) +ref.validate(); +``` + +--- + +## Components + +Components are **pure data containers** — no logic. They must implement `Component` and provide: + +1. **Default constructor** — required for registration factory +2. **Copy constructor** — used by `clone()` +3. **`clone()` method** — ECS calls this internally to duplicate data + +### Entity Component Template + +```java +public class PoisonComponent implements Component { + private float damagePerTick; + private float tickInterval; + private int remainingTicks; + private float elapsedTime; + + // Static ComponentType holder for convenient access + private static ComponentType type; + + public static ComponentType getComponentType() { + return type; + } + + public static void setComponentType(ComponentType type) { + PoisonComponent.type = type; + } + + // BuilderCodec for serialization (see hytale-persistent-data skill for full Codec reference) + public static final BuilderCodec CODEC = BuilderCodec + .builder(PoisonComponent.class, PoisonComponent::new) + .append( + new KeyedCodec<>("DamagePerTick", Codec.FLOAT), + (data, value) -> data.damagePerTick = value, + data -> data.damagePerTick + ).add() + .append( + new KeyedCodec<>("TickInterval", Codec.FLOAT), + (data, value) -> data.tickInterval = value, + data -> data.tickInterval + ).add() + .append( + new KeyedCodec<>("RemainingTicks", Codec.INTEGER), + (data, value) -> data.remainingTicks = value, + data -> data.remainingTicks + ).add() + .append( + new KeyedCodec<>("ElapsedTime", Codec.FLOAT), + (data, value) -> data.elapsedTime = value, + data -> data.elapsedTime + ).add() + .build(); + + // Default constructor (required for factory) + public PoisonComponent() { + this(5f, 1.0f, 10); + } + + // Parameterized constructor + public PoisonComponent(float damagePerTick, float tickInterval, int totalTicks) { + this.damagePerTick = damagePerTick; + this.tickInterval = tickInterval; + this.remainingTicks = totalTicks; + this.elapsedTime = 0f; + } + + // Copy constructor (required for clone) + public PoisonComponent(PoisonComponent other) { + this.damagePerTick = other.damagePerTick; + this.tickInterval = other.tickInterval; + this.remainingTicks = other.remainingTicks; + this.elapsedTime = other.elapsedTime; + } + + @Nullable + @Override + public Component clone() { + return new PoisonComponent(this); + } + + // Getters, setters, utility methods... +} +``` + +> **Important:** KeyedCodec identifier strings must be **Uppercase** and **globally unique across your entire mod**. See `hytale-persistent-data` skill for full Codec reference including validators, MapCodec, and complex types. + +### Block Component Template + +Block components use `Component` instead of `Component`: + +```java +public class ExampleBlock implements Component { + public static final BuilderCodec CODEC = BuilderCodec + .builder(ExampleBlock.class, ExampleBlock::new) + .build(); + + public ExampleBlock() { } + + public static ComponentType getComponentType() { + return ExamplePlugin.get().getExampleBlockComponentType(); + } + + @Nullable + public Component clone() { + return new ExampleBlock(); + } +} +``` + +### Accessing Components + +Always use `Store` to access component data — never call methods directly on entity objects: + +```java +// In a command, system, or event handler with store + ref: +Player player = store.getComponent(ref, Player.getComponentType()); +UUIDComponent uuid = store.getComponent(ref, UUIDComponent.getComponentType()); +TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + +player.sendMessage(Message.raw("Position: " + transform.getPosition())); +``` + +### Player Components + +Players are composed of two key components: + +| Component | Lifetime | Purpose | +|-----------|----------|---------| +| `PlayerRef` | While connected to server (survives world switches) | Connection identity: username, UUID, language, packet handler | +| `Player` | While spawned in a world (per-world) | Physical presence, gameplay-specific data | + +--- + +## Systems + +Systems contain **all logic**. They operate on entities matching component queries. The ECS scheduler runs systems each tick. + +### EntityTickingSystem + +Most common type. Runs every tick, processes each matching entity individually. + +```java +public class PoisonSystem extends EntityTickingSystem { + private final ComponentType poisonComponentType; + + public PoisonSystem(ComponentType poisonComponentType) { + this.poisonComponentType = poisonComponentType; + } + + @Override + public void tick(float dt, int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + PoisonComponent poison = archetypeChunk.getComponent(index, poisonComponentType); + Ref ref = archetypeChunk.getReferenceTo(index); + + poison.addElapsedTime(dt); + if (poison.getElapsedTime() >= poison.getTickInterval()) { + poison.resetElapsedTime(); + Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.OUT_OF_WORLD, poison.getDamagePerTick()); + DamageSystems.executeDamage(ref, commandBuffer, damage); + poison.decrementRemainingTicks(); + } + if (poison.isExpired()) { + commandBuffer.removeComponent(ref, poisonComponentType); + } + } + + @Nullable + @Override + public SystemGroup getGroup() { + return DamageModule.get().getGatherDamageGroup(); + } + + @Nonnull + @Override + public Query getQuery() { + return Query.and(this.poisonComponentType); + } +} +``` + +**Key parameters:** +- `dt` — delta time since last tick (use for time accumulation, not tick counting) +- `index` — position in the archetype chunk +- `archetypeChunk` — access entity components via index +- `commandBuffer` — queue changes (thread-safe) + +### TickingSystem + +Runs once per tick **globally**, not per-entity. Use for world-wide logic. + +```java +public class GlobalUpdateSystem extends TickingSystem { + @Override + public void tick(float dt, int index, Store store) { + World world = store.getExternalData().getWorld(); + // Global logic here + } +} +``` + +### DelayedEntitySystem + +Like `EntityTickingSystem` but with a built-in interval. Constructor takes seconds between executions. + +```java +public class HealthRegenSystem extends DelayedEntitySystem { + public HealthRegenSystem() { + super(1.0f); // Runs every 1 second + } + + @Override + public void tick(float dt, int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + // Runs every 1 second per matching entity + } + + @Nonnull + @Override + public Query getQuery() { + return Query.and(Player.getComponentType()); + } +} +``` + +### RefChangeSystem (RefSystem) + +Reacts to component add/set/remove events. Use for caching, side effects, and initialization logic. + +```java +public class MyRefSystem extends RefChangeSystem { + @Nonnull + @Override + public ComponentType componentType() { + return MyComponent.getComponentType(); + } + + @Override + public void onComponentAdded(@Nonnull Ref ref, + @Nonnull MyComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + // Component was added to entity + } + + @Override + public void onComponentSet(@Nonnull Ref ref, + @Nullable MyComponent oldComponent, + @Nonnull MyComponent newComponent, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + // Component was updated via replaceComponent or putComponent + } + + @Override + public void onComponentRemoved(@Nonnull Ref ref, + @Nonnull MyComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + // Component was removed from entity + } + + @Nullable + @Override + public Query getQuery() { + return MyComponent.getComponentType(); + } +} +``` + +--- + +## Queries + +Queries filter which entities a system processes. Only matching entities reach `tick()`. + +```java +// Single component — any entity with PoisonComponent +Query.and(poisonComponentType) + +// Multiple components — entities with BOTH +Query.and(poisonComponentType, Player.getComponentType()) + +// Exclusion — players that aren't dead +Query.and(Player.getComponentType(), Query.not(DeathComponent.getComponentType())) +``` + +--- + +## CommandBuffer + +Queues changes instead of mutating the Store directly. **Always use CommandBuffer** for thread safety and proper ordering. + +```java +// Add a component +commandBuffer.addComponent(ref, componentType, new MyComponent()); + +// Remove a component +commandBuffer.removeComponent(ref, componentType); + +// Read a component (safe within system tick) +MyComponent comp = commandBuffer.getComponent(ref, componentType); +``` + +--- + +## SystemGroups and Dependencies + +Controls execution order. Critical for systems that interact (e.g., damage pipeline). + +### Declaring a Group + +```java +@Nullable +@Override +public SystemGroup getGroup() { + return DamageModule.get().getGatherDamageGroup(); +} +``` + +### Declaring Dependencies + +```java +@Nonnull +public Set> getDependencies() { + return Set.of( + new SystemGroupDependency(Order.AFTER, DamageModule.get().getFilterDamageGroup()), + new SystemDependency(Order.BEFORE, PlayerSystems.ProcessPlayerInput.class) + ); +} +``` + +### Damage Pipeline Stages (Example) + +Hytale's damage system demonstrates why ordering matters: + +1. **GatherDamageGroup** — Collects damage sources +2. **FilterDamageGroup** — Applies reductions, cancellations (armor, invulnerability) +3. **Apply** — Damage applied to health +4. **InspectDamageGroup** — Side effects (particles, sounds, death animations) + +Wrong order = death animations before entity dies, or armor applied after health subtracted. + +--- + +## Block Components (ChunkStore) + +Block components use `ChunkStore` instead of `EntityStore`. They require a different registration path and additional setup for ticking. + +### Block RefSystem (Initializer) + +Reacts when block entities with your component are added. Use to mark blocks as ticking: + +```java +public class ExampleInitializer extends RefSystem { + @Override + public void onEntityAdded(@Nonnull Ref ref, @Nonnull AddReason reason, + @Nonnull Store store, @Nonnull CommandBuffer commandBuffer) { + BlockModule.BlockStateInfo info = (BlockModule.BlockStateInfo) commandBuffer + .getComponent(ref, BlockModule.BlockStateInfo.getComponentType()); + if (info == null) return; + + ExampleBlock generator = (ExampleBlock) commandBuffer + .getComponent(ref, ExamplePlugin.get().getExampleBlockComponentType()); + if (generator != null) { + int x = ChunkUtil.xFromBlockInColumn(info.getIndex()); + int y = ChunkUtil.yFromBlockInColumn(info.getIndex()); + int z = ChunkUtil.zFromBlockInColumn(info.getIndex()); + + WorldChunk worldChunk = (WorldChunk) commandBuffer + .getComponent(info.getChunkRef(), WorldChunk.getComponentType()); + if (worldChunk != null) { + worldChunk.setTicking(x, y, z, true); + } + } + } + + @Override + public void onEntityRemove(@Nonnull Ref ref, @Nonnull RemoveReason reason, + @Nonnull Store store, @Nonnull CommandBuffer commandBuffer) { } + + @Override + public Query getQuery() { + return Query.and(BlockModule.BlockStateInfo.getComponentType(), + ExamplePlugin.get().getExampleBlockComponentType()); + } +} +``` + +### Block Ticking System + +Processes ticking blocks each tick: + +```java +public class ExampleSystem extends EntityTickingSystem { + public void tick(float dt, int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + BlockSection blocks = (BlockSection) archetypeChunk + .getComponent(index, BlockSection.getComponentType()); + if (blocks.getTickingBlocksCountCopy() != 0) { + ChunkSection section = (ChunkSection) archetypeChunk + .getComponent(index, ChunkSection.getComponentType()); + BlockComponentChunk blockComponentChunk = (BlockComponentChunk) commandBuffer + .getComponent(section.getChunkColumnReference(), BlockComponentChunk.getComponentType()); + + blocks.forEachTicking(blockComponentChunk, commandBuffer, section.getY(), + (bcc, cb, localX, localY, localZ, blockId) -> { + Ref blockRef = bcc + .getEntityReference(ChunkUtil.indexBlockInColumn(localX, localY, localZ)); + if (blockRef == null) return BlockTickStrategy.IGNORED; + + ExampleBlock exampleBlock = (ExampleBlock) cb + .getComponent(blockRef, ExampleBlock.getComponentType()); + if (exampleBlock != null) { + WorldChunk worldChunk = (WorldChunk) commandBuffer + .getComponent(section.getChunkColumnReference(), WorldChunk.getComponentType()); + World world = worldChunk.getWorld(); + int globalX = localX + (worldChunk.getX() * 32); + int globalZ = localZ + (worldChunk.getZ() * 32); + + // Must execute setBlock on world thread + world.execute(() -> { + world.setBlock(globalX + 1, localY, globalZ, "Rock_Ice"); + }); + return BlockTickStrategy.CONTINUE; + } + return BlockTickStrategy.IGNORED; + }); + } + } + + @Nullable + public Query getQuery() { + return Query.and(BlockSection.getComponentType(), ChunkSection.getComponentType()); + } +} +``` + +**Key points:** +- `worldChunk.setTicking(x, y, z, true)` marks a block for ticking +- `BlockTickStrategy.CONTINUE` keeps it ticking next tick; `IGNORED` skips +- `world.execute(() -> ...)` schedules work on the world thread (cannot call store functions from a system directly) +- Coordinate conversion: `globalX = localX + (worldChunk.getX() * 32)` + +--- + +## Plugin Registration + +Components and systems must be registered during the plugin lifecycle. + +### EntityStore Registration (Entities) + +```java +public final class ExamplePlugin extends JavaPlugin { + private static ExamplePlugin instance; + private ComponentType poisonComponent; + + public ExamplePlugin(@Nonnull JavaPluginInit init) { + super(init); + instance = this; + } + + @Override + protected void setup() { + // Register components in setup() — returns ComponentType handle + this.poisonComponent = this.getEntityStoreRegistry() + .registerComponent(PoisonComponent.class, PoisonComponent::new); + PoisonComponent.setComponentType(this.poisonComponent); + + // Register commands, events, etc. + this.getCommandRegistry().registerCommand(new ExampleCommand()); + this.getEventRegistry().registerGlobal(PlayerReadyEvent.class, ExampleEvent::onPlayerReady); + } + + @Override + protected void start() { + // Register systems in start() + this.getEntityStoreRegistry().registerSystem(new PoisonSystem(PoisonComponent.getComponentType())); + } + + public ComponentType getPoisonComponentType() { + return poisonComponent; + } + + public static ExamplePlugin get() { return instance; } +} +``` + +### ChunkStore Registration (Blocks) + +```java +@Override +protected void setup() { + this.exampleBlockComponentType = this.getChunkStoreRegistry() + .registerComponent(ExampleBlock.class, "ExampleBlock", ExampleBlock.CODEC); +} + +@Override +protected void start() { + this.getChunkStoreRegistry().registerSystem(new ExampleSystem()); + this.getChunkStoreRegistry().registerSystem(new ExampleInitializer()); +} +``` + +### Block Module Dependencies + +If working with block components, add dependencies in `manifest.json` to ensure proper load order: + +```json +{ + "Dependencies": { + "Hytale:EntityModule": "*", + "Hytale:BlockModule": "*" + } +} +``` + +Without these, you'll get `NullPointerException: Cannot invoke "Query.validateRegistry"` on startup. + +--- + +## Best Practices + +1. **Never store direct entity references** — always use `Ref` handles +2. **Use CommandBuffer** for all entity/component mutations (thread safety) +3. **Keep components as pure data** — no logic in components +4. **Store ComponentType as a static field** on the component class for easy access +5. **Use queries to filter** — don't check component existence inside `tick()` +6. **Use SystemGroups/Dependencies** to control execution order +7. **Use `dt` (delta time)** for time-based logic — don't count ticks +8. **Register components in `setup()`** and systems in `start()` +9. **Use `world.execute(() -> ...)`** when calling world/store functions from block systems +10. **Reference `FarmingSystems.Ticking`** in Hytale source for block ticking patterns + +## External References + +- [ECS Introduction](https://hytalemodding.dev/en/docs/guides/ecs/entity-component-system) +- [Hytale ECS Theory](https://hytalemodding.dev/en/docs/guides/ecs/hytale-ecs-theory) +- [Systems Guide](https://hytalemodding.dev/en/docs/guides/ecs/systems) +- [Example ECS Plugin](https://hytalemodding.dev/en/docs/guides/ecs/example-ecs-plugin) +- [Block Components](https://hytalemodding.dev/en/docs/guides/ecs/block-components) +``` diff --git a/skills/hytale-entity-effects/SKILL.md b/skills/hytale-entity-effects/SKILL.md new file mode 100644 index 0000000..24ede17 --- /dev/null +++ b/skills/hytale-entity-effects/SKILL.md @@ -0,0 +1,945 @@ +--- +name: hytale-entity-effects +description: Documents Hytale's Entity Effect system for applying status effects, buffs, debuffs, DoTs, and visual effects to entities. Use when creating effects for NPCs, applying effects via affixes, working with EffectControllerComponent, or defining effect JSON. Triggers - effect, entity effect, status effect, debuff, buff, DoT, burn, stun, slow, poison, freeze, root, ApplicationEffects, EffectControllerComponent, effect JSON. +--- + +# Hytale Entity Effects System + +This skill provides comprehensive documentation for Hytale's Entity Effect system, including JSON schema, API usage, and integration patterns. + +## Quick Reference + +| Task | Approach | +|------|----------| +| Create status effect | JSON in `Server/Entity/Effects/Status/` | +| Apply effect to entity | `EffectControllerComponent.addEffect(ref, entityEffect, accessor)` | +| Remove effect from entity | `EffectControllerComponent.removeEffect(ref, effectIndex, accessor)` | +| Create damage-over-time | `DamageCalculator` + `DamageCalculatorCooldown` in effect JSON | +| Disable movement | `ApplicationEffects.MovementEffects.DisableAll: true` | +| Disable abilities | `ApplicationEffects.AbilityEffects.Disabled: [...]` | +| Apply visual tint | `ApplicationEffects.EntityBottomTint` / `EntityTopTint` | +| Add particles | `ApplicationEffects.Particles: [{ "SystemId": "..." }]` | + +## Effect Categories + +Hytale organizes effects into subdirectories by purpose: + +| Directory | Purpose | Examples | +|-----------|---------|----------| +| `Status/` | Status effects (debuffs/buffs) | Burn, Stun, Slow, Poison, Freeze, Root | +| `Potion/` | Consumable effects | Health regen, Stamina regen, Morph | +| `Movement/` | Movement-related effects | Dodge invulnerability | +| `Immunity/` | Damage resistance effects | Fire immunity, Environmental immunity | +| `Weapons/` | Weapon ability effects | Signature moves, special attacks | +| `Damage/` | Visual damage feedback | Red flash on hit | +| `Food/` | Food consumption effects | Nutrition buffs | +| `Stamina/` | Stamina system effects | Stamina drain, recovery | + +--- + +## JSON Schema Reference + +### Core EntityEffect Properties + +```json +{ + "Name": "Effect_Name", + "Duration": 10, + "Infinite": false, + "Debuff": true, + "OverlapBehavior": "Overwrite", + "RemovalBehavior": "Duration", + "Invulnerable": false, + "StatusEffectIcon": "UI/StatusEffects/Icon.png", + "Locale": "effect.custom.name", + "ApplicationEffects": { }, + "DamageCalculator": { }, + "DamageCalculatorCooldown": 1, + "DamageEffects": { }, + "StatModifiers": { }, + "ValueType": "Absolute", + "DamageResistance": { }, + "ModelChange": "ModelName", + "ModelOverride": { } +} +``` + +### Property Reference + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `Name` | string | - | Localization key for display name | +| `Duration` | float | 0 | Duration in seconds | +| `Infinite` | boolean | false | Effect persists until explicitly removed | +| `Debuff` | boolean | false | True for negative effects, false for buffs | +| `OverlapBehavior` | enum | `IGNORE` | Behavior when effect is reapplied | +| `RemovalBehavior` | enum | `COMPLETE` | How the effect ends | +| `Invulnerable` | boolean | false | Makes entity invulnerable while active | +| `StatusEffectIcon` | string | - | Path to UI icon (relative to Common/) | +| `Locale` | string | - | Translation key for death cause | + +### OverlapBehavior Values + +| Value | Description | +|-------|-------------| +| `EXTEND` | Adds new duration to remaining duration | +| `OVERWRITE` | Replaces with new duration | +| `IGNORE` | Keeps existing effect, ignores new application | + +### RemovalBehavior Values + +| Value | Description | +|-------|-------------| +| `COMPLETE` | Standard removal when duration expires | +| `INFINITE` | Never removed by duration | +| `DURATION` | Explicitly duration-based | + +--- + +## ApplicationEffects Schema + +`ApplicationEffects` defines visual, audio, and gameplay effects while the effect is active. + +```json +{ + "ApplicationEffects": { + "EntityBottomTint": "#000000", + "EntityTopTint": "#ff0000", + "EntityAnimationId": "Hurt", + "ScreenEffect": "ScreenEffects/Fire.png", + "HorizontalSpeedMultiplier": 0.5, + "KnockbackMultiplier": 0, + "LocalSoundEventId": "SFX_Effect_Burn_Local", + "WorldSoundEventId": "SFX_Effect_Burn_World", + "ModelVFXId": "Burn", + "MouseSensitivityAdjustmentTarget": 0.25, + "MouseSensitivityAdjustmentDuration": 0.1, + "Particles": [ ], + "FirstPersonParticles": [ ], + "MovementEffects": { }, + "AbilityEffects": { } + } +} +``` + +### Visual Properties + +| Property | Type | Description | +|----------|------|-------------| +| `EntityBottomTint` | color | Hex color tint for bottom of entity | +| `EntityTopTint` | color | Hex color tint for top of entity | +| `EntityAnimationId` | string | Animation to trigger on entity | +| `ScreenEffect` | string | Screen overlay effect path | +| `ModelVFXId` | string | VFX model to attach to entity | + +### Gameplay Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `HorizontalSpeedMultiplier` | float | 1.0 | Multiplier for movement speed (0.5 = 50% speed) | +| `KnockbackMultiplier` | float | 1.0 | Multiplier for knockback received | +| `MouseSensitivityAdjustmentTarget` | float | - | Target mouse sensitivity (0-1) | +| `MouseSensitivityAdjustmentDuration` | float | - | Time to reach target sensitivity | + +### Audio Properties + +| Property | Type | Description | +|----------|------|-------------| +| `LocalSoundEventId` | string | Sound played to affected player only | +| `WorldSoundEventId` | string | Sound played to all nearby players | +| `WorldRemovalSoundEventId` | string | Sound when effect ends (world) | +| `LocalRemovalSoundEventId` | string | Sound when effect ends (local) | + +--- + +## Particles + +Particles attach visual effects to entity bones/nodes. + +### ModelParticle Schema + +```json +{ + "Particles": [ + { + "SystemId": "Effect_Fire", + "TargetEntityPart": "Entity", + "TargetNodeName": "Head", + "Color": "#ff0000", + "Scale": 1.0, + "PositionOffset": { "X": 0, "Y": 0, "Z": 0 }, + "RotationOffset": { }, + "DetachedFromModel": false + } + ] +} +``` + +### Particle Properties + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `SystemId` | string | Yes | Particle system identifier | +| `TargetEntityPart` | enum | Yes | Where to attach (`Entity`, `Self`) | +| `TargetNodeName` | string | No | Bone/node name to attach to | +| `Color` | color | No | Tint color for particles | +| `Scale` | float | No | Size multiplier (default: 1.0) | +| `PositionOffset` | Vector3f | No | Position offset from node | +| `RotationOffset` | Direction | No | Rotation offset | +| `DetachedFromModel` | boolean | No | If true, particles spawn in world space | + +--- + +## MovementEffects Schema + +Disables specific movement inputs while effect is active. + +```json +{ + "MovementEffects": { + "DisableAll": true, + "DisableForward": false, + "DisableBackward": false, + "DisableLeft": false, + "DisableRight": false, + "DisableSprint": false, + "DisableJump": false, + "DisableCrouch": false + } +} +``` + +**Note:** Setting `DisableAll: true` automatically enables all individual disable flags. + +### Movement Effect Examples + +**Stun (complete immobilization):** +```json +{ + "MovementEffects": { + "DisableAll": true + } +} +``` + +**Root (no walking, can still look around):** +```json +{ + "MovementEffects": { + "DisableAll": true + }, + "AbilityEffects": { + "Disabled": [] + } +} +``` + +--- + +## AbilityEffects Schema + +Disables specific ability types while effect is active. + +```json +{ + "AbilityEffects": { + "Disabled": [ + "Primary", + "Secondary", + "Ability1", + "Ability2", + "Ability3" + ] + } +} +``` + +### Ability Types + +| Value | Description | +|-------|-------------| +| `Primary` | Primary attack/action (LMB) | +| `Secondary` | Secondary attack/action (RMB) | +| `Ability1` | First ability slot | +| `Ability2` | Second ability slot | +| `Ability3` | Third ability slot | + +--- + +## DamageCalculator Schema + +Applies periodic damage while effect is active. + +```json +{ + "DamageCalculator": { + "Type": "Absolute", + "Class": "Unknown", + "BaseDamage": { + "Fire": 5, + "Poison": 10 + }, + "RandomPercentageModifier": 0.1, + "SequentialModifierStep": 0, + "SequentialModifierMinimum": 0 + }, + "DamageCalculatorCooldown": 1 +} +``` + +### DamageCalculator Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Type` | enum | `Absolute` or `Percent` | +| `Class` | enum | Damage class for modifier application | +| `BaseDamage` | map | Damage amounts keyed by damage type | +| `RandomPercentageModifier` | float | Random variance (+/-) | +| `SequentialModifierStep` | float | Damage change per tick | +| `SequentialModifierMinimum` | float | Minimum damage after modifiers | + +### DamageCalculatorCooldown + +Time in seconds between damage ticks. Example: `"DamageCalculatorCooldown": 1` = damage every 1 second. + +### Common Damage Types + +| Type | Description | +|------|-------------| +| `Fire` | Fire/burning damage | +| `Poison` | Poison damage | +| `Ice` | Cold/frost damage | +| `Lightning` | Electric damage | +| `Physical` | Physical damage | + +--- + +## DamageEffects Schema + +Visual/audio feedback when damage is dealt by the effect. + +```json +{ + "DamageEffects": { + "WorldSoundEventId": "SFX_Effect_Burn_World", + "PlayerSoundEventId": "SFX_Effect_Burn_Local", + "ModelParticles": [ ], + "WorldParticles": [ ], + "Knockback": { }, + "CameraEffect": "CameraEffect_Name", + "StaminaDrainMultiplier": 1.0 + } +} +``` + +--- + +## StatModifiers Schema + +Apply stat changes while effect is active. + +```json +{ + "StatModifiers": { + "Health": 50, + "MoveSpeed": -25, + "AttackDamage": 10 + }, + "ValueType": "Percent" +} +``` + +### ValueType + +| Value | Description | +|-------|-------------| +| `Absolute` | Direct value change (100 = max value) | +| `Percent` | Percentage change | + +--- + +## DamageResistance Schema + +Apply damage resistance while effect is active. + +```json +{ + "DamageResistance": { + "Fire": [ + { + "Amount": 1.0, + "CalculationType": "Multiplicative" + } + ] + } +} +``` + +An `Amount` of `1.0` with `Multiplicative` calculation = 100% damage reduction (immunity). + +--- + +## ModelChange / ModelOverride + +Change entity appearance while effect is active. + +### Simple Model Change +```json +{ + "ModelChange": "Corgi" +} +``` + +### Complex Model Override (with animations) +```json +{ + "ModelOverride": { + "Model": "VFX/Spells/Roots/Model.blockymodel", + "Texture": "VFX/Spells/Roots/Model.png", + "AnimationSets": { + "Spawn": { + "Animations": [ + { + "Animation": "VFX/Spells/Roots/Spawn.blockyanim", + "Looping": false + } + ] + }, + "Despawn": { + "Animations": [ + { + "Animation": "VFX/Spells/Roots/Despawn.blockyanim", + "Looping": false + } + ] + } + } + } +} +``` + +--- + +## Complete Effect Examples + +### Burn Effect (DoT + Visuals) +```json +{ + "ApplicationEffects": { + "EntityBottomTint": "#100600", + "EntityTopTint": "#cf2302", + "ScreenEffect": "ScreenEffects/Fire.png", + "WorldSoundEventId": "SFX_Effect_Burn_World", + "LocalSoundEventId": "SFX_Effect_Burn_Local", + "Particles": [{ "SystemId": "Effect_Fire" }], + "ModelVFXId": "Burn" + }, + "DamageCalculatorCooldown": 1, + "DamageCalculator": { + "BaseDamage": { "Fire": 5 } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Effect_Burn_World", + "PlayerSoundEventId": "SFX_Effect_Burn_Local" + }, + "OverlapBehavior": "Overwrite", + "Debuff": true, + "StatusEffectIcon": "UI/StatusEffects/Burn.png", + "Duration": 3 +} +``` + +### Stun Effect (CC + Disable) +```json +{ + "Duration": 10, + "ApplicationEffects": { + "EntityBottomTint": "#ffa93f", + "ScreenEffect": "ScreenEffects/Snow.png", + "Particles": [{ + "SystemId": "Stunned", + "TargetEntityPart": "Entity", + "TargetNodeName": "Head" + }], + "EntityTopTint": "#da72ff", + "MovementEffects": { + "DisableAll": true + }, + "AbilityEffects": { + "Disabled": ["Primary", "Secondary", "Ability1", "Ability3"] + } + } +} +``` + +### Slow Effect (Speed Reduction) +```json +{ + "Duration": 10, + "ApplicationEffects": { + "HorizontalSpeedMultiplier": 0.5 + } +} +``` + +### Root Effect (Immobilize + Visuals) +```json +{ + "Duration": 10, + "ModelOverride": { + "Model": "VFX/Spells/Roots/Model.blockymodel", + "Texture": "VFX/Spells/Roots/Model.png", + "AnimationSets": { + "Spawn": { + "Animations": [{ + "Animation": "VFX/Spells/Roots/Spawn.blockyanim", + "Looping": false + }] + }, + "Despawn": { + "Animations": [{ + "Animation": "VFX/Spells/Roots/Despawn.blockyanim", + "Looping": false + }] + } + } + }, + "ApplicationEffects": { + "EntityTopTint": "#008000", + "EntityBottomTint": "#000000", + "ScreenEffect": "ScreenEffects/Poison.png", + "Particles": [{ "SystemId": "Effect_Poison" }], + "MovementEffects": { "DisableAll": true }, + "KnockbackMultiplier": 0 + } +} +``` + +### Regen Effect (Healing + Visuals) +```json +{ + "StatModifiers": { "Health": 50 }, + "ValueType": "Percent", + "DamageCalculatorCooldown": 5, + "Duration": 5.05, + "OverlapBehavior": "Overwrite", + "StatusEffectIcon": "Icons/ItemsGenerated/Potion_Health.png", + "ApplicationEffects": { + "Particles": [{ + "SystemId": "Potion_Health_Heal", + "TargetEntityPart": "Entity", + "TargetNodeName": "Pelvis" + }] + }, + "StatModifierEffects": { + "WorldParticles": [{ "SystemId": "Potion_Health_Implosion" }], + "WorldSoundEventId": "SFX_Deployable_Totem_Heal_Despawn" + } +} +``` + +### Morph Effect (Model Change) +```json +{ + "StatusEffectIcon": "Icons/ItemsGenerated/Potion_Purify.png", + "OverlapBehavior": "Overwrite", + "Duration": 60, + "ModelChange": "Corgi", + "ApplicationEffects": { + "Particles": [{ "SystemId": "Potion_Morph_Burst" }], + "WorldSoundEventId": "SFX_Wolf_Alerted" + } +} +``` + +### Immunity Effect (Damage Resistance) +```json +{ + "Infinite": true, + "DamageResistance": { + "Fire": [{ + "Amount": 1.0, + "CalculationType": "Multiplicative" + }] + } +} +``` + +--- + +## Java API Reference + +### EffectControllerComponent + +The component that manages active effects on an entity. + +#### Getting the Component +```java +ComponentType effectType = + EffectControllerComponent.getComponentType(); +EffectControllerComponent effects = componentAccessor.getComponent(entityRef, effectType); +``` + +#### Adding Effects +```java +// Get effect from asset registry +EntityEffect burnEffect = EntityEffect.getAssetMap().getAsset("Status/Burn"); + +// Add effect with default duration +effects.addEffect(entityRef, burnEffect, componentAccessor); + +// Add effect with custom duration +effects.addEffect(entityRef, burnEffect, 5.0f, OverlapBehavior.EXTEND, componentAccessor); + +// Add infinite effect +effects.addInfiniteEffect(entityRef, effectIndex, entityEffect, componentAccessor); +``` + +#### Removing Effects +```java +int effectIndex = EntityEffect.getAssetMap().getIndex("Status/Burn"); +effects.removeEffect(entityRef, effectIndex, RemovalBehavior.COMPLETE, componentAccessor); +``` + +#### Checking Effect State +```java +// Check if effect is active +boolean hasBurn = effects.hasEffect(burnEffectIndex); + +// Check invulnerability +boolean isInvulnerable = effects.isInvulnerable(); + +// Set invulnerability +effects.setInvulnerable(true); +``` + +### EntityEffect Asset Access +```java +// Get asset store +AssetStore store = EntityEffect.getAssetStore(); + +// Get asset map for lookups +IndexedLookupTableAssetMap assetMap = EntityEffect.getAssetMap(); + +// Get effect by ID +EntityEffect effect = assetMap.getAsset("Status/Burn"); + +// Get effect index (for fast lookups) +int effectIndex = assetMap.getIndex("Status/Burn"); +``` + +--- + +## Projectile System + +Projectiles are used by triggered effects (like `spawn_projectile` in affixes) and require **two JSON files** to function. + +### Required Files + +| File Type | Location | Purpose | +|-----------|----------|---------| +| Projectile Definition | `Server//Projectiles/.json` | Physics, damage, explosions, sounds | +| Model Asset | `Server//Models/Projectiles/.json` | Visual appearance, particles, light | + +### How They Connect + +``` +Affix "spawn_projectile" Effect + └─ "ProjectileId": "hyforged:meteor" + └─ Server/Hyforged/Projectiles/Meteor.json + └─ "Appearance": "Hyforged/Meteor" + └─ Server/Hyforged/Models/Projectiles/Meteor.json + └─ "Model": "Items/Projectiles/Fireball.blockymodel" +``` + +### Projectile Definition Schema + +Location: `Server/Hyforged/Projectiles/.json` + +```json +{ + "Appearance": "Hyforged/Meteor", + "Radius": 0.3, + "Height": 0.3, + "MuzzleVelocity": 30, + "TerminalVelocity": 80, + "Gravity": 15, + "Bounciness": 0, + "ImpactSlowdown": 0, + "SticksVertically": false, + "TimeToLive": 10, + "Damage": 80, + "DeadTime": 0.1, + "HitSoundEventId": "SFX_Fireball_Hit", + "MissSoundEventId": "SFX_Fireball_Miss", + "DeathSoundEventId": "SFX_Explosion", + "HitParticles": { "SystemId": "Impact_Fire" }, + "MissParticles": { "SystemId": "Explosion_Medium" }, + "DeathParticles": { "SystemId": "Explosion_Large" }, + "DeathEffectsOnHit": true, + "ExplosionConfig": { } +} +``` + +### Projectile Properties Reference + +| Property | Type | Description | +|----------|------|-------------| +| `Appearance` | string | Reference to Model Asset (without .json) | +| `Radius` | double | Collision radius | +| `Height` | double | Collision height | +| `MuzzleVelocity` | double | Initial speed when spawned | +| `TerminalVelocity` | double | Maximum speed | +| `Gravity` | double | Gravity strength (0 = no drop) | +| `Bounciness` | double | 0-1, how much it bounces off surfaces | +| `SticksVertically` | boolean | Stick to surfaces on hit | +| `TimeToLive` | double | Seconds before auto-despawn (0 = instant on miss) | +| `Damage` | integer | Base damage on hit | +| `DeadTime` | double | Delay before despawn after hit | +| `DeathEffectsOnHit` | boolean | Play death effects when hitting entity | + +### Shot Properties + +| Property | Type | Description | +|----------|------|-------------| +| `VerticalCenterShot` | double | Vertical spawn offset | +| `HorizontalCenterShot` | double | Horizontal spawn offset | +| `DepthShot` | double | Forward spawn offset | +| `PitchAdjustShot` | boolean | Adjust for pitch when spawning | +| `ComputeYaw` | boolean | Rotate to face travel direction | +| `ComputePitch` | boolean | Pitch to match trajectory | +| `ComputeRoll` | boolean | Apply roll based on velocity | + +### ExplosionConfig Schema + +For area-of-effect damage on impact: + +```json +{ + "ExplosionConfig": { + "DamageEntities": true, + "DamageBlocks": false, + "BlockDamageRadius": 0, + "EntityDamageRadius": 5, + "EntityDamageFalloff": 1.0, + "Knockback": { + "Type": "Point", + "Force": 8, + "VelocityType": "Set", + "VelocityConfig": { + "AirResistance": 0.97, + "GroundResistance": 0.94, + "Threshold": 3.0 + } + } + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `DamageEntities` | boolean | Damage entities in radius | +| `DamageBlocks` | boolean | Damage/destroy blocks | +| `EntityDamageRadius` | double | Radius for entity damage | +| `EntityDamageFalloff` | double | Damage reduction over distance | +| `Knockback.Type` | string | `Point` (away from center) or `Direction` | +| `Knockback.Force` | double | Knockback strength | + +--- + +## Model Asset Schema (Projectiles) + +Location: `Server/Hyforged/Models/Projectiles/.json` + +```json +{ + "Model": "Items/Projectiles/Fireball.blockymodel", + "Texture": "Items/Projectiles/Fireball.png", + "HitBox": { + "Max": { "X": 0.3, "Y": 0.3, "Z": 0.3 }, + "Min": { "X": -0.3, "Y": -0.3, "Z": -0.3 } + }, + "MinScale": 2, + "MaxScale": 2, + "Particles": [ + { + "SystemId": "Fire_Projectile", + "TargetNodeName": "" + } + ], + "Light": { + "Color": "#ff4400" + } +} +``` + +### Model Asset Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Model` | string | Path to .blockymodel file | +| `Texture` | string | Path to texture file | +| `HitBox` | object | Visual bounding box | +| `MinScale` / `MaxScale` | double | Random scale range | +| `Particles` | array | Attached particle systems | +| `Light` | object | Dynamic light emitted | + +### Reusing Existing Models + +You can reference Hytale's built-in models: +- `Items/Projectiles/Fireball.blockymodel` — Fire orb +- `Items/Projectiles/Projectile.blockymodel` — Generic projectile +- `Items/Projectiles/Tornado.blockymodel` — Tornado effect + +Or create custom models in `Common/Models/`. + +--- + +## Complete Projectile Examples + +### Meteor (Large AOE Fire) + +**Projectile:** `Server/Hyforged/Projectiles/Meteor.json` +```json +{ + "Appearance": "Hyforged/Meteor", + "Radius": 0.5, + "Height": 0.5, + "MuzzleVelocity": 25, + "TerminalVelocity": 60, + "Gravity": 20, + "TimeToLive": 15, + "Damage": 80, + "DeadTime": 0, + "MissSoundEventId": "SFX_Explosion", + "DeathSoundEventId": "SFX_Explosion", + "MissParticles": { "SystemId": "Explosion_Large" }, + "DeathParticles": { "SystemId": "Explosion_Large" }, + "DeathEffectsOnHit": true, + "ExplosionConfig": { + "DamageEntities": true, + "EntityDamageRadius": 4, + "EntityDamageFalloff": 0.5, + "Knockback": { "Type": "Point", "Force": 10 } + } +} +``` + +**Model:** `Server/Hyforged/Models/Projectiles/Meteor.json` +```json +{ + "Model": "Items/Projectiles/Fireball.blockymodel", + "Texture": "Items/Projectiles/Fireball.png", + "HitBox": { + "Max": { "X": 0.5, "Y": 0.5, "Z": 0.5 }, + "Min": { "X": -0.5, "Y": -0.5, "Z": -0.5 } + }, + "MinScale": 3, + "MaxScale": 3, + "Particles": [ + { "SystemId": "Fire_Projectile", "TargetNodeName": "" }, + { "SystemId": "Smoke_Trail", "TargetNodeName": "" } + ], + "Light": { "Color": "#ff6600" } +} +``` + +### Arcane Bolt (Fast Multi-Shot) + +**Projectile:** `Server/Hyforged/Projectiles/ArcaneBolt.json` +```json +{ + "Appearance": "Hyforged/ArcaneBolt", + "Radius": 0.1, + "Height": 0.2, + "MuzzleVelocity": 50, + "TerminalVelocity": 50, + "Gravity": 0, + "TimeToLive": 5, + "Damage": 15, + "DeadTime": 0, + "HitSoundEventId": "SFX_Magic_Impact", + "HitParticles": { "SystemId": "Impact_Arcane" }, + "MissParticles": { "SystemId": "Impact_Arcane" } +} +``` + +**Model:** `Server/Hyforged/Models/Projectiles/ArcaneBolt.json` +```json +{ + "Model": "Items/Projectiles/Fireball.blockymodel", + "Texture": "Items/Projectiles/Fireball_Textures/Void.png", + "HitBox": { + "Max": { "X": 0.15, "Y": 0.15, "Z": 0.15 }, + "Min": { "X": -0.15, "Y": -0.15, "Z": -0.15 } + }, + "MinScale": 0.8, + "MaxScale": 1.0, + "Particles": [ + { "SystemId": "Effect_Arcane", "TargetNodeName": "" } + ], + "Light": { "Color": "#9900ff" } +} +``` + +--- + +## Integration with Hyforged Affixes + +When creating NPC affixes that apply effects, use the `apply_effect` triggered effect: + +```json +{ + "Id": "hyforged:poisonous", + "Type": "npc_rare", + "DisplayName": "Poisonous", + "Description": "Attacks poison enemies.", + "TriggeredEffects": [ + { + "Trigger": "on_hit", + "Effect": "apply_effect", + "Params": { + "effect_id": "Status/Poison", + "duration": 8, + "chance": 0.25 + } + } + ] +} +``` + +### Custom Effect for Affixes + +If Hytale's built-in effects don't match your needs, create custom effects: + +1. Create effect JSON in `src/main/resources/Server/Hyforged/Entity/Effects/` +2. Reference with `hyforged:Effect_Name` in affix `apply_effect` + +```json +{ + "Id": "hyforged:arcane-burn", + "DisplayName": "effect.hyforged.arcane_burn.name", + "Duration": 5, + "Debuff": true, + "DamageCalculatorCooldown": 0.5, + "DamageCalculator": { + "BaseDamage": { "Arcane": 15 } + }, + "ApplicationEffects": { + "EntityTopTint": "#9900ff", + "EntityBottomTint": "#330066", + "Particles": [{ "SystemId": "Effect_Arcane" }] + } +} +``` + +--- + +## Best Practices + +1. **Keep effects modular** - Separate visual effects from gameplay effects when possible +2. **Use OverlapBehavior wisely** - `EXTEND` for stacking DoTs, `OVERWRITE` for refreshing CC +3. **Provide visual feedback** - Always include tints/particles for debuffs so players know what's affecting them +4. **Test duration carefully** - Balance duration against DamageCalculatorCooldown for DoTs +5. **Consider immunity frames** - Use `Invulnerable: true` for short dodge effects to prevent chain-stunning +6. **Namespace custom effects** - Use `hyforged:` prefix for Hyforged-specific effects diff --git a/skills/hytale-env-setup/SKILL.md b/skills/hytale-env-setup/SKILL.md new file mode 100644 index 0000000..697d2ca --- /dev/null +++ b/skills/hytale-env-setup/SKILL.md @@ -0,0 +1,413 @@ +--- +name: hytale-env-setup +description: Guides setting up a Hytale plugin development environment and building/testing mods. Covers JDK 25 installation (Windows/macOS/Linux), VS Code configuration, Gradle wrapper, project template, build commands, deploying to the Mods folder, and troubleshooting. Use when a user needs to set up their environment, clone the plugin template, fix build errors, or deploy a plugin for testing. Triggers - setup, environment, env, JDK, Java 25, install, Gradle, build, test, deploy, Mods folder, plugin template, gradlew, build.gradle, settings.gradle, gradle.properties, hytale.home_path, VS Code, development environment. +--- + +# Hytale Environment Setup & Build/Test + +Step-by-step guide for setting up a Hytale plugin development environment and building/testing plugins. Focused on **VS Code** (skip JetBrains). + +> **Source:** [Setting Up Your Development Environment](https://hytalemodding.dev/en/docs/guides/plugin/setting-up-env) | [Build and Test Your Mod](https://hytalemodding.dev/en/docs/guides/plugin/build-and-test) + +--- + +## Prerequisites + +- Windows 10/11, macOS, or Linux +- At least 8 GB RAM +- 10 GB free disk space +- Administrative privileges + +--- + +## Step 1: Install JDK 25 + +Hytale modding requires **Java 25** (OpenJDK recommended from [Adoptium](https://adoptium.net/)). + +### Windows (Installer) + +1. Download OpenJDK 25 from [Adoptium](https://adoptium.net/). +2. Run the installer with default settings. + - **Enable** "Set or Override JAVA_HOME variable" to avoid version conflicts. +3. Verify in a **new** terminal: + ``` + java -version + ``` + +### Windows (Scoop) + +```powershell +# Install Scoop if you don't have it (https://scoop.sh/) +scoop bucket add java +scoop install java/openjdk25 +``` + +### macOS (Homebrew) + +```bash +brew install openjdk@25 +``` + +If `java --version` fails, add to PATH: + +```bash +echo 'export PATH="$(brew --prefix)/opt/openjdk@25/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc +``` + +### Linux (Ubuntu/Debian) + +```bash +sudo apt update +sudo apt install openjdk-25-jdk +``` + +### Verify + +``` +java -version +``` + +Should show Java 25 or later. + +--- + +## Step 2: Install VS Code + Java Extensions + +1. Install [VS Code](https://code.visualstudio.com/). +2. Install the **Extension Pack for Java** (Microsoft) — includes: + - Language Support for Java (Red Hat) + - Debugger for Java + - Maven for Java + - Gradle for Java + - Test Runner for Java +3. Install the **Gradle for Java** extension if not already included. + +### VS Code Java Settings + +Ensure VS Code uses JDK 25. In `settings.json`: + +```json +{ + "java.configuration.runtimes": [ + { + "name": "JavaSE-25", + "path": "C:\\Program Files\\Eclipse Adoptium\\jdk-25", + "default": true + } + ] +} +``` + +Adjust the path to match your JDK installation. + +--- + +## Step 3: Clone the Plugin Template + +The official Hytale plugin template includes a Gradle Wrapper, so you do **not** need a system-wide Gradle install. + +```bash +git clone https://github.com/HytaleModding/plugin-template.git MyFirstMod +cd MyFirstMod +``` + +Or download the ZIP from the repository page and extract it. + +### Open in VS Code + +```bash +code MyFirstMod +``` + +VS Code with the Java extension pack will automatically detect the Gradle project, download dependencies, and set up the classpath. + +--- + +## Step 4: Configure the Project + +### `settings.gradle.kts` + +Set your project name: + +```kotlin +rootProject.name = "MyPlugin" +``` + +### `build.gradle.kts` + +The template's build script handles: +- Hytale dependency resolution +- Manifest packaging +- JAR output + +### `gradle.properties` + +If your Hytale installation is not at the default location, create or edit `gradle.properties` in the project root: + +```properties +hytale.home_path=C:\Path\To\Hytale +``` + +### `manifest.json` + +Edit `src/main/resources/manifest.json` with your plugin details. See the `hytale-plugin-config` skill for manifest structure. + +--- + +## Step 5: Build + +Open a terminal in the project root and run: + +```bash +./gradlew build +``` + +On Windows (if not using Git Bash): + +```powershell +.\gradlew.bat build +``` + +This will: +1. Compile Java source code +2. Run tests (if present) +3. Package into a JAR in `build/libs/` + +### Build Output + +The JAR will be at: + +``` +build/libs/MyPlugin-1.0.jar +``` + +The exact name depends on `rootProject.name` and `version` in your build files. + +### Common Build Error: Hytale Not Found + +``` +FAILURE: Build failed with an exception. +* What went wrong: +Failed to find Hytale at the expected location. +``` + +**Fix:** Set the correct path in `gradle.properties`: + +```properties +hytale.home_path=C:\Correct\Path\To\Hytale +``` + +--- + +## Step 6: Deploy & Test + +### 1. Locate the Mods Folder + +Default path on Windows: + +``` +C:\Users\\AppData\Roaming\Hytale\UserData\Mods +``` + +> **Tip:** Press `Win + R`, type `%appdata%`, navigate to `Hytale\UserData\Mods`. Create the `Mods` folder if it doesn't exist. + +### 2. Copy the JAR + +Copy `build/libs/MyPlugin-1.0.jar` into the `Mods` folder. + +### 3. Launch & Verify + +1. Start Hytale +2. Click "Create a New World" +3. Click the settings cog +4. Click "Mods" +5. Your mod should appear in the list + +--- + +## Hytale Maven Repository + +If setting up from scratch (not using the template), add the Hytale dependency: + +### Gradle (`build.gradle.kts`) + +```kotlin +repositories { + mavenCentral() + maven { + name = "hytale" + url = uri("https://maven.hytale.com/release") + // Or "https://maven.hytale.com/pre-release" for pre-release + } +} + +dependencies { + implementation("com.hypixel.hytale:Server:+") // latest version +} +``` + +### Maven (`pom.xml`) + +```xml + + + hytale-release + https://maven.hytale.com/release + + + + + + com.hypixel.hytale + Server + LATEST_VERSION_HERE + provided + + +``` + +--- + +## Step 7: VS Code Tasks + +Create `.vscode/tasks.json` to enable one-click build and deploy from VS Code. **Before creating the deploy task, ask the user where Hytale is installed** so the deploy path can be configured correctly. + +### Setup Procedure + +1. **Ask the user for their Hytale installation path** (e.g., `C:\Program Files\Hytale` or wherever they installed it). +2. **Derive the Mods folder** from that path: `\UserData\Mods` (Windows) or check `%APPDATA%\Hytale\UserData\Mods` for the default AppData location. +3. **Set `hytale.home_path`** in `gradle.properties` to the user's Hytale path. +4. **Create `.vscode/tasks.json`** with the tasks below, substituting the deploy path. + +### `.vscode/tasks.json` + +```json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build plugin", + "type": "shell", + "command": ".\\gradlew.bat", + "args": ["build"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$javac"], + "detail": "Build the plugin JAR via Gradle" + }, + { + "label": "build and deploy", + "type": "shell", + "command": ".\\gradlew.bat", + "args": ["build"], + "group": "build", + "problemMatcher": ["$javac"], + "detail": "Build the plugin and copy to Hytale Mods folder", + "dependsOrder": "sequence", + "dependsOn": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "deploy to mods", + "type": "shell", + "command": "powershell", + "args": [ + "-Command", + "Copy-Item -Path 'build/libs/*.jar' -Destination '${config:hytale.modsPath}' -Force" + ], + "problemMatcher": [], + "detail": "Copy built JAR to Hytale Mods folder", + "dependsOn": ["build plugin"] + } + ] +} +``` + +> **Note:** The `build and deploy` task can alternatively be a single compound task. The example above uses a separate `deploy to mods` task that depends on `build plugin`. + +### `.vscode/settings.json` (Hytale Mods Path) + +Store the Mods folder path as a VS Code setting so tasks can reference it via `${config:hytale.modsPath}`: + +```json +{ + "hytale.modsPath": "C:\\Users\\\\AppData\\Roaming\\Hytale\\UserData\\Mods" +} +``` + +**Replace** `` with the actual username, or use the full path the user provides. + +### macOS / Linux Alternative + +For non-Windows, replace the deploy command: + +```json +{ + "command": "cp", + "args": ["build/libs/*.jar", "${config:hytale.modsPath}"] +} +``` + +And use `./gradlew` instead of `.\\gradlew.bat` in the build tasks. + +### `gradle.properties` + +Also set the Hytale home path so Gradle can find the game libraries: + +```properties +hytale.home_path=C:\Path\To\Hytale +``` + +--- + +## First-Time Setup Flow (For Agents) + +When a user asks for help and the project is not yet set up, follow this flow: + +1. **Check for `.vscode/tasks.json`** — if missing, environment is likely not configured. +2. **Check for `gradle.properties`** — if missing or lacks `hytale.home_path`, need to configure. +3. **Check Java version** — run `java -version` in terminal. +4. **Ask the user:** + - "Where is Hytale installed on your system?" (e.g., `C:\Program Files\Hytale`) + - This is needed for both `gradle.properties` (build) and `.vscode/settings.json` (deploy) +5. **Derive the Mods path:** + - Windows default: `%APPDATA%\Hytale\UserData\Mods` + - Or under the install path: `\UserData\Mods` + - Confirm with the user if unclear. +6. **Create/update files:** + - `gradle.properties` with `hytale.home_path` + - `.vscode/tasks.json` with build + deploy tasks + - `.vscode/settings.json` with `hytale.modsPath` +7. **Verify** the build works: run the `build plugin` task. + +--- + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| `java -version` shows wrong version | Ensure JAVA_HOME points to JDK 25; restart terminal | +| Gradle can't find Hytale | Set `hytale.home_path` in `gradle.properties` | +| JAR not in `build/libs/` | Run `./gradlew build` and check for compile errors | +| Mod not in Hytale mod list | Verify JAR is in `/Hytale/UserData/Mods`, check `manifest.json` | +| VS Code doesn't recognize Java | Install Extension Pack for Java; verify `java.configuration.runtimes` | +| Gradle wrapper missing | Run `gradle wrapper` or re-clone the template | + +--- + +## Environment Verification Checklist + +Use this to validate a user's environment is ready: + +1. `java -version` → Java 25+ +2. VS Code installed with Extension Pack for Java +3. Project opens without errors in VS Code +4. `./gradlew build` succeeds +5. JAR exists in `build/libs/` +6. Hytale `Mods` folder exists and is accessible diff --git a/skills/hytale-events/SKILL.md b/skills/hytale-events/SKILL.md new file mode 100644 index 0000000..3ed9b99 --- /dev/null +++ b/skills/hytale-events/SKILL.md @@ -0,0 +1,526 @@ +--- +name: hytale-events +description: Documents Hytale's event system for handling game events in plugins. Covers IEvent (global events), IAsyncEvent (async events), and EcsEvent (ECS entity/block events). Use when listening to player join/disconnect, chat, crafting, damage, block break/place, entity death, or any server event. Triggers - event, IEvent, IAsyncEvent, EcsEvent, CancellableEcsEvent, EntityEventSystem, EventRegistry, registerGlobal, registerAsync, PlayerReadyEvent, PlayerDisconnectEvent, PlayerChatEvent, BreakBlockEvent, PlaceBlockEvent, Damage, CraftRecipeEvent, DropItemEvent, DeathSystems, OnDeathSystem, event handler, event listener. +--- + +# Hytale Events Skill + +Use this skill when working with events in Hytale plugins. This covers the three event categories: **IEvent** (global synchronous events), **IAsyncEvent** (asynchronous events), and **EcsEvent** (ECS-based entity/block events). + +> **Related skills:** For chat-specific event handling, see `hytale-chat-formatting`. For ECS system fundamentals, see `hytale-ecs`. For entity effects triggered by events, see `hytale-entity-effects`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Listen to player join | `registerGlobal(PlayerReadyEvent.class, handler)` in `setup()` | +| Listen to player disconnect | `registerGlobal(PlayerDisconnectEvent.class, handler)` in `setup()` | +| Listen to chat messages | `registerAsync(PlayerChatEvent.class, handler)` in `setup()` | +| Cancel block break | Extend `EntityEventSystem`, call `setCancelled(true)` | +| Cancel crafting | Extend `EntityEventSystem` | +| Handle entity damage | Extend `EntityEventSystem` | +| Handle player death | Extend `DeathSystems.OnDeathSystem` | +| Handle block placement | Extend `EntityEventSystem` | +| Register global event | `this.getEventRegistry().registerGlobal(EventClass.class, Handler::method)` | +| Register async event | `this.getEventRegistry().registerAsync(EventClass.class, Handler::method)` | +| Register ECS event system | `this.getEntityStoreRegistry().registerSystem(new MyEventSystem())` in `start()` | + +--- + +## Event Categories + +Hytale has three distinct event types, each with different registration patterns: + +| Category | Interface | Registration | Cancellable | Use Case | +|----------|-----------|--------------|-------------|----------| +| **IEvent** | `IEvent` | `registerGlobal()` | No | Player join, disconnect, world events, plugin lifecycle | +| **IAsyncEvent** | `IAsyncEvent` | `registerAsync()` | Yes (some) | Chat messages, asset loading | +| **EcsEvent** | `EcsEvent` / `CancellableEcsEvent` | `registerSystem()` | Yes (Cancellable) | Block break/place, damage, crafting, item drops | + +--- + +## IEvent — Global Events + +Global events are fired for server-wide occurrences. Register them in your plugin's `setup()` method. + +### Registration Pattern + +```java +@Override +protected void setup() { + this.getEventRegistry().registerGlobal(PlayerReadyEvent.class, MyEventHandler::onPlayerReady); + this.getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, MyEventHandler::onPlayerDisconnect); + this.getEventRegistry().registerGlobal(ShutdownEvent.class, MyEventHandler::onShutdown); +} +``` + +### Handler Class + +```java +public class MyEventHandler { + public static void onPlayerReady(PlayerReadyEvent event) { + Player player = event.getPlayer(); + player.sendMessage(Message.raw("Welcome " + player.getDisplayName())); + } + + public static void onPlayerDisconnect(PlayerDisconnectEvent event) { + // event.getRef() returns a Ref for the disconnecting player + } + + public static void onShutdown(ShutdownEvent event) { + // Clean up resources on server shutdown + } +} +``` + +### Required Imports (IEvent) + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.event.player.PlayerReadyEvent; +import com.hypixel.hytale.server.event.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.event.ShutdownEvent; +``` + +### Available IEvent Types + +#### Player Events +| Event | Description | Key Methods | +|-------|-------------|-------------| +| `PlayerReadyEvent` | Player finished joining (fully loaded) | `getPlayer()` | +| `PlayerDisconnectEvent` | Player disconnecting | `getRef()` → `Ref` | +| `PlayerConnectEvent` | Player beginning connection | - | +| `PlayerSetupConnectEvent` | Player setup phase start | - | +| `PlayerSetupDisconnectEvent` | Player setup phase disconnect | - | +| `PlayerMouseButtonEvent` | Player mouse button input | - | +| `PlayerMouseMotionEvent` | Player mouse motion | - | + +#### World Events +| Event | Description | +|-------|-------------| +| `AddWorldEvent` | A world is being added | +| `RemoveWorldEvent` | A world is being removed | +| `StartWorldEvent` | A world is starting | +| `AddPlayerToWorldEvent` | Player added to a world | +| `DrainPlayerFromWorldEvent` | Player removed from a world | +| `AllWorldsLoadedEvent` | All worlds finished loading | + +#### Lifecycle Events +| Event | Description | +|-------|-------------| +| `BootEvent` | Server boot | +| `ShutdownEvent` | Server shutting down | +| `PluginSetupEvent` | Plugin setup phase | +| `AllNPCsLoadedEvent` | All NPCs finished loading | +| `LoadedNPCEvent` | Individual NPC loaded | + +#### Asset Events +| Event | Description | +|-------|-------------| +| `AssetPackRegisterEvent` | Asset pack registered | +| `AssetPackUnregisterEvent` | Asset pack unregistered | +| `RegisterAssetStoreEvent` | Asset store registered | +| `RemoveAssetStoreEvent` | Asset store removed | +| `GenerateAssetsEvent` | Assets being generated | +| `LoadedAssetsEvent` | Assets finished loading | +| `RemovedAssetsEvent` | Assets removed | +| `LoadAssetEvent` | Individual asset loading | + +#### Other Events +| Event | Description | +|-------|-------------| +| `EntityRemoveEvent` | Entity removed from world | +| `LivingEntityInventoryChangeEvent` | Entity inventory changed | +| `ItemContainerChangeEvent` | Item container changed | +| `GenerateDefaultLanguageEvent` | Default language generation | +| `GenerateSchemaEvent` | Schema generation | +| `GenerateServerStateEvent` | Server state generation | +| `ChunkPreLoadProcessEvent` | Chunk pre-load processing | +| `TreasureChestOpeningEvent` | Treasure chest opened | +| `WindowCloseEvent` | Window closed | +| `WorldPathChangedEvent` | World path changed | +| `MessagesUpdated` | Messages updated | + +--- + +## IAsyncEvent — Asynchronous Events + +Async events run off the main tick thread. Register in `setup()` using `registerAsync()`. + +### Registration Pattern + +```java +@Override +protected void setup() { + this.getEventRegistry().registerAsync(PlayerChatEvent.class, MyEventHandler::onPlayerChat); +} +``` + +### Handler Example + +```java +public static void onPlayerChat(PlayerChatEvent event) { + PlayerRef sender = event.getSender(); + String content = event.getContent(); + + // Cancel the message + if (content.contains("badword")) { + event.setCancelled(true); + sender.sendMessage(Message.raw("That word is not allowed!")); + return; + } + + // Modify content + event.setContent(content.toUpperCase()); + + // Set custom formatter + event.setFormatter((playerRef, message) -> + Message.join( + Message.raw("[Server] ").color(Color.GOLD), + Message.raw(sender.getUsername()).color(Color.WHITE), + Message.raw(": " + message).color(Color.GRAY) + )); +} +``` + +### Available IAsyncEvent Types + +| Event | Description | Cancellable | +|-------|-------------|-------------| +| `PlayerChatEvent` | Player sends a chat message | Yes | +| `SendCommonAssetsEvent` | Common assets being sent | No | +| `AssetEditorFetchAutoCompleteDataEvent` | Editor autocomplete data | No | +| `AssetEditorRequestDataSetEvent` | Editor data set request | No | + +--- + +## EcsEvent — ECS Entity/Block Events + +ECS events are fired within the ECS tick loop and operate on entities matching a query. They use `EntityEventSystem` and are registered as systems. + +### Key Difference from IEvent + +- **IEvent**: Simple handler function, registered in `setup()` +- **EcsEvent**: Full ECS system class extending `EntityEventSystem`, registered in `start()` via `registerSystem()` +- **EcsEvent** receives `Store`, `CommandBuffer`, and `ArchetypeChunk` — giving full ECS access + +### EntityEventSystem Pattern + +```java +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Archetype; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.ecs.system.EntityEventSystem; +import javax.annotation.Nonnull; + +class MyCraftHandler extends EntityEventSystem { + + public MyCraftHandler() { + super(CraftRecipeEvent.Pre.class); + } + + @Override + public void handle(int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer, + @Nonnull CraftRecipeEvent.Pre event) { + // Access entity ref + var ref = archetypeChunk.getReferenceTo(index); + + // Access event data + CraftingRecipe recipe = event.getCraftedRecipe(); + + // Cancel if needed (CancellableEcsEvent only) + event.setCancelled(true); + } + + @Override + public Query getQuery() { + // Return which entities this system processes + // Archetype.empty() = all entities that receive this event + return Archetype.empty(); + } +} +``` + +### Registration + +```java +@Override +protected void start() { + this.getEntityStoreRegistry().registerSystem(new MyCraftHandler()); +} +``` + +### Required Imports (EcsEvent) + +```java +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.Archetype; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.ecs.system.EntityEventSystem; +import javax.annotation.Nonnull; +``` + +### CancellableEcsEvent Types + +These events extend `CancellableEcsEvent` and support `setCancelled(true)`: + +| Event | Sub-Events | Description | +|-------|------------|-------------| +| `BreakBlockEvent` | — | Player breaking a block | +| `PlaceBlockEvent` | — | Player placing a block | +| `ChangeGameModeEvent` | — | Game mode change | +| `ChunkSaveEvent` | — | Chunk saving | +| `ChunkUnloadEvent` | — | Chunk unloading | +| `CraftRecipeEvent` | `.Pre`, `.Post` | Recipe crafted (Pre = before, Post = after) | +| `Damage` | — | Entity taking damage | +| `DamageBlockEvent` | — | Block being damaged | +| `DropItemEvent` | `.Drop`, `.PlayerRequest` | Item dropped | +| `InteractivelyPickupItemEvent` | — | Player picking up an item | +| `PrefabPasteEvent` | — | Prefab being pasted | +| `SwitchActiveSlotEvent` | — | Active hotbar slot switch | + +### Non-Cancellable EcsEvent Types + +| Event | Sub-Events | Description | +|-------|------------|-------------| +| `DiscoverInstanceEvent` | `.Display` | Instance discovered | +| `DiscoverZoneEvent` | `.Display` | Zone discovered | +| `MoonPhaseChangeEvent` | — | Moon phase changed | +| `UseBlockEvent` | `.Pre`, `.Post` | Block used (Pre = before, Post = after) | + +--- + +## Special Pattern: Death Events + +Player/entity death uses a specialized system extending `DeathSystems.OnDeathSystem` (a `RefChangeSystem` under the hood). + +### Death System Example + +```java +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.server.core.Message; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.modules.entity.damage.Damage; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathSystems; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import javax.annotation.Nonnull; + +public class PlayerDeathHandler extends DeathSystems.OnDeathSystem { + + @Nonnull + @Override + public Query getQuery() { + // Only process player deaths + return Query.and(Player.getComponentType()); + } + + @Override + public void onComponentAdded( + @Nonnull Ref ref, + @Nonnull DeathComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + + Player playerComponent = (Player) store.getComponent(ref, Player.getComponentType()); + assert playerComponent != null; + + Universe.get().sendMessage( + Message.raw("Player died: " + playerComponent.getDisplayName())); + + // Access death damage info + Damage deathInfo = component.getDeathInfo(); + if (deathInfo != null) { + Universe.get().sendMessage( + Message.raw("Damage amount: " + deathInfo.getAmount())); + } + } +} +``` + +### Death System Registration + +```java +@Override +protected void start() { + this.getEntityStoreRegistry().registerSystem(new PlayerDeathHandler()); +} +``` + +--- + +## Complete Plugin Example + +A full plugin demonstrating all three event categories: + +```java +import com.hypixel.hytale.server.plugin.JavaPlugin; +import com.hypixel.hytale.server.plugin.JavaPluginInit; +import com.hypixel.hytale.server.event.player.PlayerReadyEvent; +import com.hypixel.hytale.server.event.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.event.PlayerChatEvent; +import javax.annotation.Nonnull; + +public class MyPlugin extends JavaPlugin { + + public MyPlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + // IEvent — global events + this.getEventRegistry().registerGlobal(PlayerReadyEvent.class, + EventHandlers::onPlayerReady); + this.getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, + EventHandlers::onPlayerDisconnect); + + // IAsyncEvent — async events + this.getEventRegistry().registerAsync(PlayerChatEvent.class, + EventHandlers::onPlayerChat); + } + + @Override + protected void start() { + // EcsEvent — ECS event systems + this.getEntityStoreRegistry().registerSystem(new CraftBlocker()); + this.getEntityStoreRegistry().registerSystem(new PlayerDeathHandler()); + } +} +``` + +--- + +## Common Patterns + +### Cancel an Event Conditionally + +```java +// CancellableEcsEvent pattern +@Override +public void handle(int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer, + @Nonnull BreakBlockEvent event) { + // Check condition, then cancel + if (shouldPreventBreak(event)) { + event.setCancelled(true); + } +} +``` + +### Block Crafting by Ingredient + +```java +class BlockFibreCrafting extends EntityEventSystem { + + public BlockFibreCrafting() { + super(CraftRecipeEvent.Pre.class); + } + + @Override + public void handle(int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer, + @Nonnull CraftRecipeEvent.Pre event) { + CraftingRecipe recipe = event.getCraftedRecipe(); + if (recipe.getInput() != null) { + for (MaterialQuantity mq : recipe.getInput()) { + if (Objects.equals(mq.getItemId(), "Ingredient_Fibre")) { + event.setCancelled(true); + break; + } + } + } + } + + @Override + public Query getQuery() { + return Archetype.empty(); + } +} +``` + +### Filter Events by Entity Type + +```java +@Override +public Query getQuery() { + // Only process for entities with the Player component + return Query.and(Player.getComponentType()); +} +``` + +### Access Entity Ref from EcsEvent + +```java +@Override +public void handle(int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Damage damageEvent) { + Ref ref = archetypeChunk.getReferenceTo(index); + // Use ref with store to access components + Player player = (Player) store.getComponent(ref, Player.getComponentType()); +} +``` + +--- + +## Choosing the Right Event Type + +| Need | Event Category | Registration | +|------|---------------|--------------| +| Player joins / leaves | IEvent | `registerGlobal()` in `setup()` | +| Chat messages | IAsyncEvent | `registerAsync()` in `setup()` | +| Block break / place | EcsEvent | `registerSystem()` in `start()` | +| Damage / combat | EcsEvent | `registerSystem()` in `start()` | +| Crafting | EcsEvent | `registerSystem()` in `start()` | +| Item drops / pickups | EcsEvent | `registerSystem()` in `start()` | +| Entity death | Special (DeathSystems) | `registerSystem()` in `start()` | +| Server shutdown | IEvent | `registerGlobal()` in `setup()` | +| World lifecycle | IEvent | `registerGlobal()` in `setup()` | + +--- + +## Best Practices + +1. **Register IEvent/IAsyncEvent in `setup()`** and EcsEvent systems in `start()` +2. **Use method references** for cleaner registration: `EventHandler::onPlayerReady` +3. **Keep event handler classes separate** from the main plugin class for organization +4. **Use `Archetype.empty()`** for the query when you want to process all entities receiving the event +5. **Use `Query.and()`** to filter which entities your EcsEvent system processes +6. **Check `setCancelled()`** only on `CancellableEcsEvent` subclasses — non-cancellable events will not have this method +7. **Use Pre/Post sub-events** when available (e.g., `CraftRecipeEvent.Pre` vs `.Post`) to choose timing +8. **Access entity data via `Store` and `Ref`** in EcsEvent handlers — never cache entity references +9. **Use `CommandBuffer`** for mutations inside EcsEvent handlers (add/remove components) +10. **Death handling** uses `DeathSystems.OnDeathSystem` (a `RefChangeSystem`), not `EntityEventSystem` + +--- + +## Resources + +- [Creating Events Guide](https://hytalemodding.dev/en/docs/guides/plugin/creating-events) +- [Events List](https://hytalemodding.dev/en/docs/server/events) +- [Player Death Event Guide](https://hytalemodding.dev/en/docs/guides/plugin/player-death-event) +- [ECS Systems Guide](https://hytalemodding.dev/en/docs/guides/ecs/systems) diff --git a/skills/hytale-hotbar-actions/SKILL.md b/skills/hytale-hotbar-actions/SKILL.md new file mode 100644 index 0000000..56eec2d --- /dev/null +++ b/skills/hytale-hotbar-actions/SKILL.md @@ -0,0 +1,477 @@ +--- +name: hytale-hotbar-actions +description: Customizes hotbar key actions in Hytale plugins using packet filtering. Use when creating custom keybinds, ability triggers, blocking slot switches, or handling hotbar input. Triggers - hotbar, keybind, ability slot, SyncInteractionChains, PlayerPacketFilter, slot switch, custom action, SetActiveSlot, ability trigger. +--- + +# Hytale Hotbar Actions Skill + +Use this skill when implementing custom hotbar keybinds in Hytale plugins. This covers intercepting slot-switch packets, blocking default behavior, and triggering custom abilities. + +> **Note:** This pattern only works for hotbar slots. Each custom action consumes one hotbar slot. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **SyncInteractionChains** | Packet (ID 290) sent when player performs interactions | +| **PlayerPacketFilter** | Filter interface to block/allow inbound packets | +| **PlayerPacketWatcher** | Watcher interface for observing packets (read-only) | +| **SetActiveSlot** | Packet (ID 177) to force client slot selection | +| **InteractionType.SwapFrom** | Interaction type when leaving a slot | + +--- + +## Part 1: Understanding the Packet System + +When a player presses a hotbar key (1-9), the client sends a `SyncInteractionChains` packet containing `SyncInteractionChain` objects. + +### SyncInteractionChain Fields + +| Field | Description | +|-------|-------------| +| `interactionType` | Type of interaction (SwapFrom, SwapTo, Attack, etc.) | +| `activeHotbarSlot` | The slot the player is currently on | +| `data.targetSlot` | The slot the player wants to switch to | +| `initial` | Whether this is the start of a new interaction chain | + +> **Note:** Slot indices are 0-based. Key "9" = slot index 8. + +--- + +## Part 2: Packet Interception + +### Watcher vs Filter + +| Type | Interface | Can Block | Use Case | +|------|-----------|-----------|----------| +| Watcher | `PlayerPacketWatcher` | No | Logging, analytics, side effects | +| Filter | `PlayerPacketFilter` | Yes | Blocking/modifying behavior | + +Use `PlayerPacketFilter` when you need to block the slot switch. + +--- + +## Part 3: Implementation + +### Required Imports + +```java +import com.hypixel.hytale.protocol.Packet; +import com.hypixel.hytale.protocol.InteractionType; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChains; +import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot; +import com.hypixel.hytale.server.core.io.adapter.PlayerPacketFilter; +import com.hypixel.hytale.server.core.io.adapter.PacketAdapters; +import com.hypixel.hytale.server.core.io.adapter.PacketFilter; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.universe.Store; +import com.hypixel.hytale.server.core.universe.entity.EntityStore; +import com.hypixel.hytale.server.core.universe.entity.Ref; +import com.hypixel.hytale.server.player.Player; +import com.hypixel.hytale.server.world.World; +import javax.annotation.Nonnull; +import java.util.logging.Level; +``` + +### Handler Class + +```java +public class AbilitySlotHandler implements PlayerPacketFilter { + private static final int ABILITY_SLOT = 8; // Slot index 8 = Key "9" + + private final MyPlugin plugin; + + public AbilitySlotHandler(MyPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean test(@Nonnull PlayerRef playerRef, @Nonnull Packet packet) { + // Step 1: Check packet type + if (!(packet instanceof SyncInteractionChains syncPacket)) { + return false; + } + + // Step 2: Look for our trigger condition + for (SyncInteractionChain chain : syncPacket.updates) { + if (chain.interactionType == InteractionType.SwapFrom + && chain.data != null + && chain.data.targetSlot == ABILITY_SLOT + && chain.initial) { + + int originalSlot = chain.activeHotbarSlot; + + // Step 3: Trigger ability and fix client state + handleAbilityTrigger(playerRef, originalSlot); + + return true; // Block the packet + } + } + + return false; // Let packet through + } + + private void handleAbilityTrigger(PlayerRef playerRef, int originalSlot) { + Ref entityRef = playerRef.getReference(); + if (entityRef == null || !entityRef.isValid()) { + return; + } + + Store store = entityRef.getStore(); + World world = store.getExternalData().getWorld(); + + // Schedule on world thread for thread safety + world.execute(() -> { + Player playerComponent = store.getComponent(entityRef, Player.getComponentType()); + if (playerComponent == null) { + return; + } + + // Fix client desync - restore original slot + playerComponent.getInventory().setActiveHotbarSlot((byte) originalSlot); + + SetActiveSlot setActiveSlotPacket = new SetActiveSlot( + Inventory.HOTBAR_SECTION_ID, // -1 indicates the hotbar + originalSlot // The slot index to select + ); + playerRef.getPacketHandler().write(setActiveSlotPacket); + + // Your ability logic here + triggerAbility(playerRef, playerComponent); + }); + } + + private void triggerAbility(PlayerRef playerRef, Player player) { + // Example: Run a command as the player + // CommandManager.get().handleCommand(playerRef, "noon"); + + // Example: Send notification + playerRef.sendMessage(Message.raw("Ability triggered!")); + } +} +``` + +### Plugin Registration + +```java +import com.hypixel.hytale.server.core.io.adapter.PacketAdapters; +import com.hypixel.hytale.server.core.io.adapter.PacketFilter; + +public class MyPlugin extends HytaleServerPlugin { + private PacketFilter inboundFilter; + + @Override + protected void setup() { + AbilitySlotHandler handler = new AbilitySlotHandler(this); + inboundFilter = PacketAdapters.registerInbound(handler); + } + + @Override + protected void shutdown() { + if (inboundFilter != null) { + PacketAdapters.deregisterInbound(inboundFilter); + } + } +} +``` + +--- + +## Part 4: The Client Desync Problem + +### The Challenge + +The client performs slot switches locally before server confirmation: + +| Side | State | +|------|-------| +| Server | Player stays on slot 5 (packet blocked) | +| Client | Player is on slot 8 (switched locally) | + +### The Solution + +Send `SetActiveSlot` packet to force the client to the correct slot: + +```java +// Update server-side state +playerComponent.getInventory().setActiveHotbarSlot((byte) originalSlot); + +// Send packet to force client to the correct slot +SetActiveSlot setActiveSlotPacket = new SetActiveSlot( + Inventory.HOTBAR_SECTION_ID, // -1 indicates the hotbar + originalSlot // The slot index to select +); +playerRef.getPacketHandler().write(setActiveSlotPacket); +``` + +--- + +## Part 5: Thread Safety + +Packet handlers run on network threads, but entity operations must run on the world thread. Always use `world.execute()`: + +```java +Ref entityRef = playerRef.getReference(); +Store store = entityRef.getStore(); +World world = store.getExternalData().getWorld(); + +world.execute(() -> { + // Entity operations here are thread-safe + Player playerComponent = store.getComponent(entityRef, Player.getComponentType()); + // ... +}); +``` + +--- + +## Part 6: Triggering Abilities + +### Running Commands + +```java +CommandManager.get().handleCommand(playerRef, "noon"); +``` + +### Spawning Projectiles + +```java +ProjectileModule.get().spawnProjectile(/* config */); +// Use TargetUtil.getLook for player's eye position +``` + +--- + +## Part 7: Multiple Ability Slots + +```java +public class MultiAbilityHandler implements PlayerPacketFilter { + private static final int ABILITY_SLOT_1 = 6; // Key "7" + private static final int ABILITY_SLOT_2 = 7; // Key "8" + private static final int ABILITY_SLOT_3 = 8; // Key "9" + + @Override + public boolean test(@Nonnull PlayerRef playerRef, @Nonnull Packet packet) { + if (!(packet instanceof SyncInteractionChains syncPacket)) { + return false; + } + + for (SyncInteractionChain chain : syncPacket.updates) { + if (chain.interactionType == InteractionType.SwapFrom + && chain.data != null + && chain.initial) { + + int targetSlot = chain.data.targetSlot; + int originalSlot = chain.activeHotbarSlot; + + switch (targetSlot) { + case ABILITY_SLOT_1 -> { + handleAbility1(playerRef, originalSlot); + return true; + } + case ABILITY_SLOT_2 -> { + handleAbility2(playerRef, originalSlot); + return true; + } + case ABILITY_SLOT_3 -> { + handleAbility3(playerRef, originalSlot); + return true; + } + } + } + } + return false; + } +} +``` + +--- + +## Part 8: Cooldowns + +```java +private final Map cooldowns = new ConcurrentHashMap<>(); +private static final long COOLDOWN_MS = 5000; // 5 seconds + +private boolean isOnCooldown(PlayerRef playerRef) { + UUID playerId = playerRef.getUUID(); + Long lastUse = cooldowns.get(playerId); + + if (lastUse == null) { + return false; + } + + return System.currentTimeMillis() - lastUse < COOLDOWN_MS; +} + +private void startCooldown(PlayerRef playerRef) { + cooldowns.put(playerRef.getUUID(), System.currentTimeMillis()); +} + +private void handleAbilityTrigger(PlayerRef playerRef, int originalSlot) { + if (isOnCooldown(playerRef)) { + playerRef.sendMessage(Message.raw("Ability on cooldown!").color(Color.RED)); + return; + } + + startCooldown(playerRef); + // ... rest of ability logic +} +``` + +--- + +## Debugging Tips + +### Log Packet Data + +```java +plugin.getLogger().at(Level.INFO).log( + "[DEBUG] Packet: %s, type=%s, activeSlot=%d, targetSlot=%d", + playerRef.getUsername(), + chain.interactionType, + chain.activeHotbarSlot, + chain.data != null ? chain.data.targetSlot : -1 +); +``` + +### Common Issues + +| Problem | Solution | +|---------|----------| +| Nothing happens when pressing key | Check filter registration in startup logs | +| Ability fires but wrong slot selected | Verify `SetActiveSlot` is sent correctly | +| Thread/null errors | Use `entityRef.isValid()`, null-check components, use `world.execute()` | + +--- + +## Complete Example + +```java +package com.example.abilities; + +import com.hypixel.hytale.protocol.Packet; +import com.hypixel.hytale.protocol.InteractionType; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChains; +import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot; +import com.hypixel.hytale.server.HytaleServerPlugin; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.Color; +import com.hypixel.hytale.server.core.io.adapter.PacketAdapters; +import com.hypixel.hytale.server.core.io.adapter.PacketFilter; +import com.hypixel.hytale.server.core.io.adapter.PlayerPacketFilter; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Store; +import com.hypixel.hytale.server.core.universe.entity.EntityStore; +import com.hypixel.hytale.server.core.universe.entity.Ref; +import com.hypixel.hytale.server.player.Player; +import com.hypixel.hytale.server.world.World; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class AbilityPlugin extends HytaleServerPlugin { + private PacketFilter inboundFilter; + + @Override + protected void setup() { + AbilitySlotHandler handler = new AbilitySlotHandler(this); + inboundFilter = PacketAdapters.registerInbound(handler); + getLogger().info("Ability slot handler registered!"); + } + + @Override + protected void shutdown() { + if (inboundFilter != null) { + PacketAdapters.deregisterInbound(inboundFilter); + } + } +} + +class AbilitySlotHandler implements PlayerPacketFilter { + private static final int ABILITY_SLOT = 8; + private static final long COOLDOWN_MS = 3000; + + private final AbilityPlugin plugin; + private final Map cooldowns = new ConcurrentHashMap<>(); + + public AbilitySlotHandler(AbilityPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean test(@Nonnull PlayerRef playerRef, @Nonnull Packet packet) { + if (!(packet instanceof SyncInteractionChains syncPacket)) { + return false; + } + + for (SyncInteractionChain chain : syncPacket.updates) { + if (chain.interactionType == InteractionType.SwapFrom + && chain.data != null + && chain.data.targetSlot == ABILITY_SLOT + && chain.initial) { + + handleAbilityTrigger(playerRef, chain.activeHotbarSlot); + return true; + } + } + return false; + } + + private void handleAbilityTrigger(PlayerRef playerRef, int originalSlot) { + // Check cooldown + UUID playerId = playerRef.getUUID(); + Long lastUse = cooldowns.get(playerId); + if (lastUse != null && System.currentTimeMillis() - lastUse < COOLDOWN_MS) { + playerRef.sendMessage(Message.raw("Ability on cooldown!").color(Color.RED)); + fixClientSlot(playerRef, originalSlot); + return; + } + + Ref entityRef = playerRef.getReference(); + if (entityRef == null || !entityRef.isValid()) { + return; + } + + Store store = entityRef.getStore(); + World world = store.getExternalData().getWorld(); + + world.execute(() -> { + Player playerComponent = store.getComponent(entityRef, Player.getComponentType()); + if (playerComponent == null) { + return; + } + + // Fix client state + playerComponent.getInventory().setActiveHotbarSlot((byte) originalSlot); + fixClientSlot(playerRef, originalSlot); + + // Start cooldown + cooldowns.put(playerId, System.currentTimeMillis()); + + // Trigger ability + playerRef.sendMessage(Message.raw("⚡ Ability activated!").color(Color.YELLOW)); + }); + } + + private void fixClientSlot(PlayerRef playerRef, int slot) { + SetActiveSlot packet = new SetActiveSlot(Inventory.HOTBAR_SECTION_ID, slot); + playerRef.getPacketHandler().write(packet); + } +} +``` + +--- + +## References + +- [Customizing Hotbar Actions Guide](https://hytalemodding.dev/en/docs/guides/plugin/customizing-hotbar-actions) +- [Listening to Packets Guide](https://hytalemodding.dev/en/docs/guides/plugin/listening-to-packets) +- [Thread Safety: Using world.execute()](https://forum.hytalemodding.dev/d/21-thread-safety-using-worldexecute) diff --git a/skills/hytale-instances/SKILL.md b/skills/hytale-instances/SKILL.md new file mode 100644 index 0000000..2f87279 --- /dev/null +++ b/skills/hytale-instances/SKILL.md @@ -0,0 +1,331 @@ +--- +name: hytale-instances +description: Documents Hytale's Instance System for creating, managing, and teleporting players into instanced worlds using InstancesPlugin. Use when spawning instance worlds from templates, teleporting players into active or loading instances, exiting instances with return points, or safely removing instances. Triggers - instance, InstancesPlugin, spawnInstance, teleportPlayerToInstance, teleportPlayerToLoadingInstance, exitInstance, safeRemoveInstance, instance template, instanced world, instance system, dungeon instance, challenge instance, return point. +--- + +# Hytale Instance System + +Use this skill when working with instanced worlds in Hytale plugins. The `InstancesPlugin` provides a complete API for spawning instance worlds from templates, teleporting players in/out, and managing instance lifecycles. + +> **Source:** + +--- + +## Quick Reference + +| Task | Method | +|------|--------| +| Get plugin instance | `InstancesPlugin.get()` | +| Spawn a new instance | `InstancesPlugin.get().spawnInstance(name, world, returnPoint)` | +| Teleport to active instance | `InstancesPlugin.teleportPlayerToInstance(playerRef, store, instanceWorld, overrideReturn)` | +| Teleport to loading instance | `InstancesPlugin.teleportPlayerToLoadingInstance(playerRef, store, worldFuture, overrideReturn)` | +| Exit an instance | `InstancesPlugin.exitInstance(playerRef, store)` | +| Remove an instance | `InstancesPlugin.safeRemoveInstance(instanceWorld)` | + +--- + +## Key Concepts + +### Instance Templates + +Instance templates are located in your asset pack under `Server/Instances/[Name]`. Each template must contain an `instance.bson` configuration file. When `spawnInstance` is called, the template is copied and initialized as a new world in the universe. + +### Return Points + +A `Transform` that defines where the player should be teleported back to when they exit the instance. This is set during instance creation and can be optionally overridden during teleportation. + +### World Execution Context + +All instance operations (spawn, teleport, remove) should be executed within the world's execution context using `world.execute(() -> { ... })` to ensure thread safety. + +--- + +## Required Imports + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.Universe; +import com.hypixel.hytale.server.world.World; +import com.hypixel.hytale.server.world.Transform; +import com.hypixel.hytale.server.world.ISpawnProvider; +import com.hypixel.hytale.server.player.Player; +import com.hypixel.hytale.server.player.PlayerRef; +import com.hypixel.hytale.server.instances.InstancesPlugin; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +``` + +--- + +## Spawning an Instance + +Use `spawnInstance` to create a new instanced world from a template. Returns a `CompletableFuture`. + +### Method Signature + +| Parameter | Type | Description | +|-----------|------|-------------| +| `name` | `String` | Name of the instance template (matches folder under `Server/Instances/`) | +| `world` | `World` | The current world context | +| `returnPoint` | `Transform` | Where the player should return when exiting the instance | +| **Returns** | `CompletableFuture` | Future that completes with the new instance world | + +### Example + +```java +InstancesPlugin plugin = InstancesPlugin.get(); +World currentWorld = /* current world context */; +Transform returnPoint = /* where players should go when they leave */; + +// Spawn an instance named "Challenge_Combat_1" +World instanceWorld = InstancesPlugin.get() + .spawnInstance("Challenge_Combat_1", currentWorld, returnPoint) + .join(); + +Universe.get().sendMessage(Message.raw("Instance spawned: " + instanceWorld.getName())); +``` + +--- + +## Teleporting Players to Instances + +The `InstancesPlugin` provides two approaches depending on whether the instance world is already loaded or still loading. + +### Teleporting to an Active Instance + +Use when the `World` object is already available (instance has finished loading). + +```java +InstancesPlugin.teleportPlayerToInstance( + playerRef, // Ref — the player's entity reference + componentAccessor, // EntityStore — the player's component accessor (store) + instanceWorld, // World — the target instance world + null // Transform — optional override for return point (null to use original) +); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `playerRef` | `Ref` | The player's entity reference | +| `componentAccessor` | `EntityStore` | The store/component accessor for the player | +| `instanceWorld` | `World` | The active instance world to teleport into | +| `overrideReturnPoint` | `Transform` | Optional override for the return point; pass `null` to use the one set during spawn | + +### Teleporting to a Loading Instance + +Use when you have a `CompletableFuture` from `spawnInstance`. This queues the teleport to happen as soon as the world is ready. + +```java +CompletableFuture worldFuture = plugin.spawnInstance("Challenge_Combat_1", currentWorld, returnPoint); + +InstancesPlugin.teleportPlayerToLoadingInstance( + playerRef, // Ref — the player's entity reference + componentAccessor, // EntityStore — the player's component accessor (store) + worldFuture, // CompletableFuture — future from spawnInstance + null // Transform — optional override for return point (null to use original) +); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `playerRef` | `Ref` | The player's entity reference | +| `componentAccessor` | `EntityStore` | The store/component accessor for the player | +| `worldFuture` | `CompletableFuture` | The future returned by `spawnInstance` | +| `overrideReturnPoint` | `Transform` | Optional override for the return point; pass `null` to use the one set during spawn | + +--- + +## Exiting Instances + +Returns the player to their original world using the return point defined when the instance was created. + +```java +InstancesPlugin.exitInstance(playerRef, componentAccessor); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `playerRef` | `Ref` | The player's entity reference | +| `componentAccessor` | `EntityStore` | The store/component accessor for the player | + +--- + +## Managing Instance Removal + +Instances can be safely removed when no longer needed (e.g., when empty). Use `safeRemoveInstance` to cleanly shut down an instance world. + +```java +InstancesPlugin.safeRemoveInstance(instanceWorld); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `instanceWorld` | `World` | The instance world to remove | + +--- + +## Getting Player Ref and Store + +In most contexts you need the player's `Ref` and the `EntityStore`: + +```java +// From a PlayerRef +PlayerRef playerRef = Universe.get().getPlayer(playerUUID); +Ref ref = playerRef.getReference(); +EntityStore store = playerRef.getReference().getStore(); + +// From a Player object (e.g., in a command) +Player player = (Player) ctx.sender(); +Ref ref = player.getReference(); +EntityStore store = player.getReference().getStore(); +``` + +### Getting a Return Point from Spawn Provider + +```java +World world = Universe.get().getWorld(playerRef.getWorldUuid()); +ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider(); +Transform returnPoint = spawnProvider != null + ? spawnProvider.getSpawnPoint(world, playerRef.getUuid()) + : new Transform(); +``` + +--- + +## Command Examples + +### Spawn Instance Command + +```java +public class ExampleSpawnInstanceCommand extends CommandBase { + private final RequiredArg nameArg; + + public ExampleSpawnInstanceCommand() { + super("spawninstance", "Spawns a new instance from a template"); + this.nameArg = this.withRequiredArg("name", "The name of the new instance", ArgTypes.STRING); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + UUID playerUUID = ctx.sender().getUuid(); + PlayerRef playerRef = Universe.get().getPlayer(playerUUID); + World world = Universe.get().getWorld(playerRef.getWorldUuid()); + + ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider(); + Transform returnPoint = spawnProvider != null + ? spawnProvider.getSpawnPoint(world, playerRef.getUuid()) + : new Transform(); + + world.execute(() -> { + World instanceWorld = InstancesPlugin.get() + .spawnInstance(this.nameArg.get(ctx), world, returnPoint) + .join(); + Universe.get().sendMessage( + Message.raw("Instance spawned: " + instanceWorld.getName()) + ); + }); + } +} +``` + +### Enter Instance Command + +Spawns an instance and immediately teleports the player into it (using `teleportPlayerToLoadingInstance` so it works even before the world finishes loading): + +```java +public class ExampleEnterInstanceCommand extends CommandBase { + private final RequiredArg nameArg; + + public ExampleEnterInstanceCommand() { + super("enterinstance", "Spawns and enters an instance immediately"); + this.nameArg = this.withRequiredArg("name", "The name of the instance", ArgTypes.STRING); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + UUID playerUUID = ctx.sender().getUuid(); + PlayerRef playerRef = Universe.get().getPlayer(playerUUID); + World world = Universe.get().getWorld(playerRef.getWorldUuid()); + + ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider(); + Transform returnPoint = spawnProvider != null + ? spawnProvider.getSpawnPoint(world, playerRef.getUuid()) + : new Transform(); + + world.execute(() -> { + CompletableFuture worldFuture = InstancesPlugin.get() + .spawnInstance(this.nameArg.get(ctx), world, returnPoint); + + InstancesPlugin.teleportPlayerToLoadingInstance( + playerRef.getReference(), + playerRef.getReference().getStore(), + worldFuture, + null + ); + }); + } +} +``` + +### Exit Instance Command + +```java +public class ExampleExitInstanceCommand extends CommandBase { + public ExampleExitInstanceCommand() { + super("exitinstance", "Exits the current instance and returns to the previous world"); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + Player player = (Player) ctx.sender(); + World world = player.getWorld(); + + world.execute(() -> { + InstancesPlugin.exitInstance( + player.getReference(), + player.getReference().getStore() + ); + }); + } +} +``` + +### Remove Instance Command + +```java +public class ExampleRemoveInstanceCommand extends CommandBase { + private final RequiredArg worldArg; + + public ExampleRemoveInstanceCommand() { + super("removeinstance", "Safely removes an instance"); + this.worldArg = this.withRequiredArg("world", "The world to remove", ArgTypes.STRING); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + String worldName = ctx.get(this.worldArg); + World targetWorld = Universe.get().getWorld(worldName); + + if (targetWorld == null) { + ctx.sendMessage(Message.raw("World not found: " + worldName)); + return; + } + + targetWorld.execute(() -> { + InstancesPlugin.safeRemoveInstance(targetWorld); + }); + } +} +``` + +--- + +## Best Practices + +1. **Always use `world.execute()`** — Instance operations must run within the world's execution context for thread safety. +2. **Prefer `teleportPlayerToLoadingInstance`** — When spawning and immediately entering, use the loading variant to avoid blocking on `.join()` before teleporting. +3. **Use spawn provider for return points** — Get the world's `ISpawnProvider` to calculate proper return positions rather than hardcoding coordinates. +4. **Clean up instances** — Call `safeRemoveInstance` when an instance is no longer needed to free resources. +5. **Null return point override** — Pass `null` for the override parameter when you want to use the return point set during `spawnInstance`. +6. **Instance templates** — Place templates under `Server/Instances/[Name]` with an `instance.bson` configuration file. diff --git a/skills/hytale-inventory/SKILL.md b/skills/hytale-inventory/SKILL.md new file mode 100644 index 0000000..0645108 --- /dev/null +++ b/skills/hytale-inventory/SKILL.md @@ -0,0 +1,316 @@ +--- +name: hytale-inventory +description: Manages player inventories in Hytale plugins using Inventory, ItemStack, ItemContainer, and PageManager APIs. Use when accessing player inventory, creating items, adding/removing items from slots, opening inventory pages, setting durability, or attaching custom metadata to items. Triggers - inventory, ItemStack, ItemContainer, Inventory, getInventory, addItemStack, removeItemStack, PageManager, Page, getStorage, getHotbar, getArmor, getBackpack, durability, item metadata, BsonDocument, slot, inventory page. +--- + +# Hytale Inventory Management + +Use this skill when managing player inventories, creating items, opening inventory pages, or manipulating item containers in Hytale plugins. + +> **Related skills:** For persistent player data, see `hytale-persistent-data`. For notifications with item icons, see `hytale-notifications`. For hotbar key actions, see `hytale-hotbar-actions`. For inventory change events, see `hytale-events`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Get player inventory | `player.getInventory()` | +| Create an item | `new ItemStack("Stone")` or `new ItemStack("Stone", 64)` | +| Create item with metadata | `new ItemStack("Stone", 64, bsonDocument)` | +| Create item with durability | `new ItemStack(itemId, qty, durability, maxDurability, metadata)` | +| Get storage container | `inventory.getStorage()` | +| Get hotbar container | `inventory.getHotbar()` | +| Get armor container | `inventory.getArmor()` | +| Get backpack container | `inventory.getBackpack()` | +| Get utility container | `inventory.getUtility()` | +| Add item to container | `container.addItemStack(itemStack)` | +| Add item to specific slot | `container.addItemStackToSlot((short) slot, itemStack)` | +| Remove item from container | `container.removeItemStack(itemStack)` | +| Remove item from slot | `container.removeItemStackFromSlot((short) slot)` | +| Open an inventory page | `pageManager.setPage(ref, store, Page.Inventory)` | + +--- + +## Required Imports + +```java +import com.hypixel.hytale.server.item.ItemStack; +import com.hypixel.hytale.server.item.ItemContainer; +import com.hypixel.hytale.server.core.entity.entities.player.Inventory; +import com.hypixel.hytale.server.core.entity.entities.player.pages.Page; +import com.hypixel.hytale.server.core.entity.entities.player.pages.PageManager; +import com.hypixel.hytale.server.ecs.entity.EntityStore; +import com.hypixel.hytale.server.ecs.Store; +import org.bson.BsonDocument; +import org.bson.BsonString; +``` + +--- + +## Accessing the Player Inventory + +Get the `Inventory` object from a `Player` instance: + +```java +Inventory inventory = player.getInventory(); +``` + +--- + +## ItemStack + +`ItemStack` represents a stack of items with a material type, quantity, optional metadata, and optional durability. + +### Creating an ItemStack + +```java +// Basic item (quantity defaults to 1) +ItemStack item = new ItemStack("Stone"); + +// Item with quantity +ItemStack stack = new ItemStack("Stone", 64); +``` + +### ItemStack with Custom Metadata + +Attach arbitrary BSON metadata to items: + +```java +BsonDocument metadata = new BsonDocument(); +metadata.append("customData", new BsonString("value")); + +ItemStack item = new ItemStack("Stone", 64, metadata); +``` + +### ItemStack with Durability + +Create items that have durability (e.g., tools, weapons): + +```java +ItemStack sword = new ItemStack( + "DiamondSword", // itemId + 1, // quantity + 100.0, // durability + 100.0, // maxDurability + metadata // metadata (optional, can be null) +); +``` + +| Constructor Parameter | Type | Description | +|----------------------|------|-------------| +| `itemId` | `String` | The item identifier (e.g., `"Stone"`, `"DiamondSword"`) | +| `quantity` | `int` | Number of items in the stack | +| `durability` | `double` | Current durability value | +| `maxDurability` | `double` | Maximum durability value | +| `metadata` | `BsonDocument` | Optional custom metadata (nullable) | + +--- + +## ItemContainer + +`ItemContainer` represents a specific section of inventory (storage, hotbar, armor, etc.). All add/remove operations are performed on an `ItemContainer`. + +### Getting an ItemContainer + +The `Inventory` class provides methods to get individual containers: + +| Method | Returns | Description | +|--------|---------|-------------| +| `.getStorage()` | `ItemContainer` | Main storage slots | +| `.getHotbar()` | `ItemContainer` | Hotbar slots | +| `.getArmor()` | `ItemContainer` | Armor equipment slots | +| `.getBackpack()` | `ItemContainer` | Backpack slots | +| `.getUtility()` | `ItemContainer` | Utility slots | + +### Combined Containers + +For operations that span multiple containers, use combined methods: + +| Method | Combines | +|--------|----------| +| `.getCombinedEverything()` | All containers | +| `.getCombinedArmorHotbarStorage()` | Armor + Hotbar + Storage | +| `.getCombinedBackpackStorageHotbar()` | Backpack + Storage + Hotbar | +| `.getCombinedHotbarFirst()` | Hotbar (priority) + others | +| `.getCombinedStorageFirst()` | Storage (priority) + others | +| `.getCombinedArmorHotbarUtilityStorage()` | Armor + Hotbar + Utility + Storage | +| `.getCombinedHotbarUtilityConsumableStorage()` | Hotbar + Utility + Consumable + Storage | + +--- + +## Adding Items + +### Add to First Available Slot + +```java +Inventory inventory = player.getInventory(); +ItemContainer storage = inventory.getStorage(); + +ItemStack item = new ItemStack("Stone", 64); +storage.addItemStack(item); +``` + +### Add to Specific Slot + +```java +ItemContainer storage = inventory.getStorage(); +ItemStack item = new ItemStack("Stone", 32); + +storage.addItemStackToSlot((short) 4, item); +``` + +--- + +## Removing Items + +### Remove by ItemStack + +```java +Inventory inventory = player.getInventory(); +ItemContainer storage = inventory.getStorage(); + +storage.removeItemStack(item); +``` + +### Remove from Specific Slot + +```java +ItemContainer storage = inventory.getStorage(); +storage.removeItemStackFromSlot((short) 4); +``` + +--- + +## Opening Inventory Pages + +Use `PageManager` and the `Page` enum to open inventory UI screens for a player. + +### Available Pages + +| Page | Description | +|------|-------------| +| `Page.None` | Close any open page | +| `Page.Bench` | Crafting bench | +| `Page.Inventory` | Player inventory screen | +| `Page.ToolsSettings` | Tools/settings page | +| `Page.Map` | Map page | +| `Page.MachinimaEditor` | Machinima editor | +| `Page.ContentCreation` | Content creation page | +| `Page.Custom` | Custom page (for plugin UIs) | + +### Opening a Page + +```java +PageManager pageManager = player.getPageManager(); +Store store = player.getWorld().getEntityStore().getStore(); + +pageManager.setPage(player.getReference(), store, Page.Inventory); +``` + +### Closing a Page + +```java +PageManager pageManager = player.getPageManager(); +Store store = player.getWorld().getEntityStore().getStore(); + +pageManager.setPage(player.getReference(), store, Page.None); +``` + +--- + +## Common Patterns + +### Give Items on Event + +```java +public void onPlayerReady(PlayerReadyEvent event) { + var player = event.getPlayer(); + Inventory inventory = player.getInventory(); + ItemContainer hotbar = inventory.getHotbar(); + + hotbar.addItemStackToSlot((short) 0, new ItemStack("Weapon_Sword_Iron", 1)); + hotbar.addItemStackToSlot((short) 1, new ItemStack("Tool_Pickaxe_Iron", 1)); + hotbar.addItemStackToSlot((short) 2, new ItemStack("Food_Apple", 16)); +} +``` + +### Clear Inventory + +```java +public void clearPlayerInventory(Player player) { + Inventory inventory = player.getInventory(); + ItemContainer storage = inventory.getStorage(); + ItemContainer hotbar = inventory.getHotbar(); + ItemContainer armor = inventory.getArmor(); + + // Remove items from each slot + for (short i = 0; i < storage.getSize(); i++) { + storage.removeItemStackFromSlot(i); + } + for (short i = 0; i < hotbar.getSize(); i++) { + hotbar.removeItemStackFromSlot(i); + } + for (short i = 0; i < armor.getSize(); i++) { + armor.removeItemStackFromSlot(i); + } +} +``` + +### Give Item with Metadata + +```java +public void giveCustomItem(Player player, String itemId, String customTag, String value) { + BsonDocument metadata = new BsonDocument(); + metadata.append(customTag, new BsonString(value)); + + ItemStack item = new ItemStack(itemId, 1, metadata); + player.getInventory().getStorage().addItemStack(item); +} +``` + +### Give Durable Tool + +```java +public void giveTool(Player player, String toolId, double maxDurability) { + ItemStack tool = new ItemStack( + toolId, + 1, + maxDurability, + maxDurability, + null + ); + player.getInventory().getHotbar().addItemStack(tool); +} +``` + +--- + +## Related Events + +| Event | Fires When | +|-------|------------| +| `LivingEntityInventoryChangeEvent` | An entity's inventory changes | +| `ItemContainerChangeEvent` | An item container is modified | + +--- + +## Best Practices + +1. **Use the correct container** — Add items to the appropriate container (hotbar for tools, armor for equipment, storage for general items). +2. **Use combined containers for searches** — When checking if a player has an item, use `getCombinedEverything()` or the appropriate combined method. +3. **Cast slot indices to `short`** — Slot methods require `(short)` cast for the index parameter. +4. **Null-check metadata** — The metadata `BsonDocument` parameter is optional and can be `null`. +5. **Use `Page.None` to close** — Always close pages with `Page.None` when done. +6. **Localize item names** — Use translation keys for any user-facing item text. + +--- + +## References + +- [Inventory Management Guide](https://hytalemodding.dev/en/docs/guides/plugin/inventory-management) +- [Hotbar Actions Skill](../hytale-hotbar-actions/SKILL.md) — Custom hotbar key handling +- [Notifications Skill](../hytale-notifications/SKILL.md) — Item icons in notifications +- [UI Modding Skill](../hytale-ui-modding/SKILL.md) — Custom pages and UI + +``` diff --git a/skills/hytale-items/SKILL.md b/skills/hytale-items/SKILL.md new file mode 100644 index 0000000..5faeecb --- /dev/null +++ b/skills/hytale-items/SKILL.md @@ -0,0 +1,598 @@ +--- +name: hytale-items +description: Documents Hytale's item system including the Item Registry API, custom item JSON definitions, crafting recipes, custom interactions (SimpleInstantInteraction), interaction chaining (Condition, Charging, Serial, Replace), and linking interactions to items. Use when creating custom items, querying the item registry, defining crafting recipes, building item interactions, or working with ItemStack. Triggers - item, custom item, item registry, Item.getAssetMap, DefaultAssetMap, ItemStack, item JSON, item definition, crafting recipe, interaction, SimpleInstantInteraction, InteractionContext, InteractionType, item interaction, Charging, Condition, Serial, Replace, item properties, MaxStack, Categories, item ID. +--- + +# Hytale Items & Interactions + +Comprehensive reference for creating custom items, querying the item registry, defining crafting recipes, and building custom interactions in Hytale plugins. + +> **Related skills:** For persistent data/Codec patterns, see `hytale-persistent-data`. For ECS fundamentals, see `hytale-ecs`. For inventory management, see the Hytale inventory APIs. For entity effects applied by items, see `hytale-entity-effects`. + +## Quick Reference + +| Task | Approach | +|------|----------| +| Get item registry | `Item.getAssetMap()` returns `DefaultAssetMap` | +| Check if item exists | `assetMap.getAsset(id)` — check for `null` and `Item.UNKNOWN` | +| Get item properties | `item.getId()`, `item.getMaxStack()`, `item.hasBlockType()`, `item.isConsumable()` | +| List all items | `Item.getAssetMap().getAssetMap().entrySet()` | +| Define custom item | JSON in `Server/Item/Items/.json` | +| Define crafting recipe | `"Recipe"` block inside item JSON | +| Create custom interaction | Extend `SimpleInstantInteraction`, add `BuilderCodec`, register in `setup()` | +| Link interaction to item | `"Interactions"` block in item JSON with interaction type ID | +| Chain interactions | Nest `Condition`, `Charging`, `Serial`, `Replace` in JSON | + +--- + +## Item Registry API + +### Required Imports + +```java +import com.hypixel.hytale.assetstore.map.DefaultAssetMap; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +``` + +### Getting the Registry + +```java +DefaultAssetMap itemMap = Item.getAssetMap(); +``` + +### Listing All Items + +```java +DefaultAssetMap itemMap = Item.getAssetMap(); +var map = itemMap.getAssetMap(); + +for (var entry : map.entrySet()) { + String itemId = String.valueOf(entry.getKey()); + Item item = entry.getValue(); + LOGGER.atInfo().log("Item ID: " + itemId); +} + +int totalItems = map.size(); +``` + +### Checking if an Item Exists + +```java +public boolean itemExists(String itemId) { + var assetMap = Item.getAssetMap(); + if (assetMap != null) { + Item item = assetMap.getAsset(itemId); + return item != null && item != Item.UNKNOWN; + } + return false; +} +``` + +> **Important:** `Item.UNKNOWN` represents an invalid or unrecognized item. Always check for both `null` and `Item.UNKNOWN` when validating items. + +### Accessing Item Properties + +```java +var assetMap = Item.getAssetMap(); +Item item = assetMap.getAsset("Soil_Grass"); + +if (item != null && item != Item.UNKNOWN) { + String id = item.getId(); // "Soil_Grass" + int maxStack = item.getMaxStack(); // 100 + boolean isBlock = item.hasBlockType(); // true + boolean isConsumable = item.isConsumable(); // false +} +``` + +--- + +## Custom Item Definition + +### Folder Structure + +Enable asset packs in `manifest.json` by setting `IncludesAssetPack` to `true`, then create: + +``` +resources/ +├── Server/ +│ └── Item/ +│ └── Items/ +│ └── my_new_item.json # Item definition +└── Common/ + ├── Icons/ + │ └── ItemsGenerated/ + │ └── my_new_item_icon.png # Inventory icon + └── Items/ + └── my_new_item/ + ├── model.blockymodel # 3D model + └── model_texture.png # Model texture +``` + +| File | Location | Purpose | +|------|----------|---------| +| `my_new_item.json` | `Server/Item/Items` | Defines item properties and behavior | +| `my_new_item_icon.png` | `Common/Icons/ItemsGenerated` | Icon for the item in inventory | +| `model.blockymodel` | `Common/Items/my_new_item` | 3D model of the item | +| `model_texture.png` | `Common/Items/my_new_item` | Texture for the item model | + +### Item JSON Schema + +```json +{ + "TranslationProperties": { + "Name": "My New Item", + "Description": "My New Item Description" + }, + "Id": "My_New_Item", + "Icon": "Icons/ItemsGenerated/my_new_item_icon.png", + "Model": "Items/my_new_item/model.blockymodel", + "Texture": "Items/my_new_item/model_texture.png", + "Quality": "Common", + "MaxStack": 1, + "Categories": [ + "Items.Example" + ] +} +``` + +### Key Item Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Id` | `String` | Unique identifier for the item (used in registry lookups) | +| `TranslationProperties.Name` | `String` | Display name (or localization key) | +| `TranslationProperties.Description` | `String` | Item description (or localization key) | +| `Icon` | `String` | Path to inventory icon (relative to `Common/`) | +| `Model` | `String` | Path to 3D model file (relative to `Common/`) | +| `Texture` | `String` | Path to model texture (relative to `Common/`) | +| `Quality` | `String` | Item quality/rarity tier (e.g., `"Common"`) | +| `MaxStack` | `int` | Maximum stack size | +| `Categories` | `String[]` | Tags/categories for the item | + +--- + +## Crafting Recipes + +Add a `"Recipe"` block to the item JSON to make it craftable: + +```json +{ + "TranslationProperties": { "Name": "My New Item" }, + "Id": "My_New_Item", + "Icon": "Icons/ItemsGenerated/my_new_item_icon.png", + "Model": "Items/my_new_item/model.blockymodel", + "Texture": "Items/my_new_item/model_texture.png", + "Quality": "Common", + "MaxStack": 1, + "Categories": ["Items.Example"], + + "Recipe": { + "TimeSeconds": 3.5, + "Input": [ + { "ItemId": "Ingredient_1", "Quantity": 15 }, + { "ItemId": "Ingredient_2", "Quantity": 15 }, + { "ItemId": "Ingredient_3", "Quantity": 15 } + ], + "BenchRequirement": [ + { + "Id": "Workbench", + "Type": "Crafting", + "Categories": ["Workbench_Survival"] + } + ] + } +} +``` + +### Recipe Properties + +| Property | Type | Description | +|----------|------|-------------| +| `TimeSeconds` | `float` | Time in seconds to craft the item | +| `Input` | `Array` | List of ingredient items with `ItemId` and `Quantity` | +| `BenchRequirement` | `Array` | Required crafting station(s) | +| `BenchRequirement[].Id` | `String` | Bench identifier | +| `BenchRequirement[].Type` | `String` | Bench type (e.g., `"Crafting"`) | +| `BenchRequirement[].Categories` | `String[]` | Which bench tab/category | + +--- + +## Custom Interactions + +Custom interactions define what happens when a player uses an item. They are implemented in Java and linked via JSON. + +### Step 1: Create the Interaction Class + +Extend `SimpleInstantInteraction` and override `firstRun`: + +```java +import com.hypixel.hytale.server.core.asset.type.item.interaction.SimpleInstantInteraction; +import com.hypixel.hytale.server.core.asset.type.item.interaction.InteractionType; +import com.hypixel.hytale.server.core.asset.type.item.interaction.InteractionContext; +import com.hypixel.hytale.server.core.asset.type.item.interaction.CooldownHandler; +import com.hypixel.hytale.server.core.asset.type.item.interaction.Interaction; +import com.hypixel.hytale.codec.BuilderCodec; + +import javax.annotation.Nonnull; + +public class MyCustomInteraction extends SimpleInstantInteraction { + + public static final BuilderCodec CODEC = BuilderCodec.builder( + MyCustomInteraction.class, MyCustomInteraction::new, SimpleInstantInteraction.CODEC + ).build(); + + @Override + protected void firstRun( + @Nonnull InteractionType interactionType, + @Nonnull InteractionContext interactionContext, + @Nonnull CooldownHandler cooldownHandler) { + // Custom behavior when the item is used + } +} +``` + +### Step 2: Register the Interaction + +Register in your plugin's `setup()` method: + +```java +public class MyPlugin extends JavaPlugin { + + public MyPlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + this.getCodecRegistry(Interaction.CODEC) + .register("my_custom_interaction_id", MyCustomInteraction.class, MyCustomInteraction.CODEC); + } +} +``` + +### Step 3: Link Interaction to Item JSON + +Add the `"Interactions"` block to the item definition: + +```json +{ + "Id": "My_New_Item", + "Interactions": { + "Secondary": { + "Interactions": [ + { + "Type": "my_custom_interaction_id" + } + ] + } + } +} +``` + +The interaction type key (`"Secondary"`, `"Primary"`, etc.) determines which player action triggers it. + +--- + +## Full Interaction Example + +A complete interaction that sends a message with the item ID to the player: + +```java +import com.hypixel.hytale.server.core.asset.type.item.interaction.SimpleInstantInteraction; +import com.hypixel.hytale.server.core.asset.type.item.interaction.InteractionType; +import com.hypixel.hytale.server.core.asset.type.item.interaction.InteractionContext; +import com.hypixel.hytale.server.core.asset.type.item.interaction.InteractionState; +import com.hypixel.hytale.server.core.asset.type.item.interaction.CooldownHandler; +import com.hypixel.hytale.server.ecs.store.EntityStore; +import com.hypixel.hytale.server.ecs.ref.Ref; +import com.hypixel.hytale.server.ecs.store.Store; +import com.hypixel.hytale.server.ecs.CommandBuffer; +import com.hypixel.hytale.server.player.Player; +import com.hypixel.hytale.server.item.ItemStack; +import com.hypixel.hytale.server.world.World; +import com.hypixel.hytale.codec.BuilderCodec; +import com.hypixel.hytale.server.logging.HytaleLogger; +import com.hypixel.hytale.server.text.Message; + +import javax.annotation.Nonnull; + +public class SendMessageInteraction extends SimpleInstantInteraction { + + public static final BuilderCodec CODEC = BuilderCodec.builder( + SendMessageInteraction.class, SendMessageInteraction::new, SimpleInstantInteraction.CODEC + ).build(); + + public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + @Override + protected void firstRun( + @Nonnull InteractionType interactionType, + @Nonnull InteractionContext interactionContext, + @Nonnull CooldownHandler cooldownHandler) { + + CommandBuffer commandBuffer = interactionContext.getCommandBuffer(); + if (commandBuffer == null) { + interactionContext.getState().state = InteractionState.Failed; + LOGGER.atInfo().log("CommandBuffer is null"); + return; + } + + World world = commandBuffer.getExternalData().getWorld(); + Store store = commandBuffer.getExternalData().getStore(); + Ref ref = interactionContext.getEntity(); + + Player player = commandBuffer.getComponent(ref, Player.getComponentType()); + if (player == null) { + interactionContext.getState().state = InteractionState.Failed; + LOGGER.atInfo().log("Player is null"); + return; + } + + ItemStack itemStack = interactionContext.getHeldItem(); + if (itemStack == null) { + interactionContext.getState().state = InteractionState.Failed; + LOGGER.atInfo().log("ItemStack is null"); + return; + } + + player.sendMessage(Message.raw("You have used the custom item +" + itemStack.getItemId())); + } +} +``` + +### Key InteractionContext Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getCommandBuffer()` | `CommandBuffer` | Access the command buffer for entity/component changes | +| `getEntity()` | `Ref` | Reference to the entity using the item | +| `getHeldItem()` | `ItemStack` | The item being used | +| `getState()` | `InteractionStateHolder` | Get/set the interaction state (`Failed`, etc.) | + +### Key CommandBuffer Methods (from InteractionContext) + +| Method | Description | +|--------|-------------| +| `commandBuffer.getExternalData().getWorld()` | Get the current `World` | +| `commandBuffer.getExternalData().getStore()` | Get the entity `Store` | +| `commandBuffer.getComponent(ref, Type)` | Read a component from an entity | + +--- + +## Advanced Interaction Chaining + +Interactions are nestable — combine them to create complex behaviors triggered by a single item use. + +### Condition + +Check conditions before allowing the interaction to proceed: + +```json +{ + "Type": "Condition", + "Crouching": true, + "Failed": "Block_Secondary", + "Next": { + // interaction to run if condition is met + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `Crouching` | `boolean` | Only proceed if player is crouching | +| `Failed` | `String` | Interaction type to run if condition fails | +| `Next` | `Object` | Interaction to run if condition passes | + +### Charging + +Require the player to hold the interaction for a duration before it activates: + +```json +{ + "Type": "Charging", + "FailsOnDamage": true, + "HorizontalSpeedMultiplier": 0.4, + "Next": { + "2.5": { + // interaction after 2.5 seconds of charging + } + }, + "Failed": { + // interaction to run if charging fails/cancelled + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `FailsOnDamage` | `boolean` | Cancel charge if player takes damage | +| `HorizontalSpeedMultiplier` | `float` | Movement speed multiplier while charging | +| `Next` | `Object` | Map of duration (seconds) → interaction | +| `Failed` | `Object` | Interaction if charging is interrupted | + +### Serial + +Execute a sequence of interactions in order: + +```json +{ + "Type": "Serial", + "Interactions": [ + { /* first interaction */ }, + { /* second interaction */ } + ] +} +``` + +### Replace + +Replace the default interaction behavior (e.g., inherited from parent): + +```json +{ + "Type": "Replace", + "Var": "Item_Default_Interaction", + "DefaultValue": { + "Interactions": [ + { /* replacement interaction */ } + ] + } +} +``` + +### Simple + +A basic interaction that performs a single action: + +```json +{ + "Type": "Simple" +} +``` + +--- + +## Advanced Interaction Example + +Require crouching + 2.5-second charge before executing a custom interaction: + +```json +{ + "Interactions": { + "Secondary": { + "Interactions": [ + { + "Type": "Condition", + "Crouching": true, + "Failed": "Block_Secondary", + "Next": { + "Type": "Charging", + "FailsOnDamage": true, + "HorizontalSpeedMultiplier": 0.5, + "Next": { + "2.5": { + "Type": "my_custom_interaction_id" + } + }, + "Failed": { + "Type": "Simple" + } + } + } + ] + } + } +} +``` + +--- + +## Interaction Codec with Custom Fields + +If your interaction needs serialized fields, add them to the `BuilderCodec`: + +```java +public class MyParameterizedInteraction extends SimpleInstantInteraction { + + private float radius; + private String effectId; + + public static final BuilderCodec CODEC = BuilderCodec.builder( + MyParameterizedInteraction.class, MyParameterizedInteraction::new, SimpleInstantInteraction.CODEC + ) + .add("Radius", KeyedCodec.FLOAT, i -> i.radius, (i, v) -> i.radius = v) + .add("EffectId", KeyedCodec.STRING, i -> i.effectId, (i, v) -> i.effectId = v) + .build(); + + @Override + protected void firstRun( + @Nonnull InteractionType interactionType, + @Nonnull InteractionContext interactionContext, + @Nonnull CooldownHandler cooldownHandler) { + // Use this.radius and this.effectId from JSON + } +} +``` + +These fields can then be set from the item JSON: + +```json +{ + "Interactions": { + "Secondary": { + "Interactions": [ + { + "Type": "my_parameterized_interaction", + "Radius": 5.0, + "EffectId": "Burn" + } + ] + } + } +} +``` + +> **See also:** `hytale-persistent-data` skill for full Codec/BuilderCodec/KeyedCodec reference. + +--- + +## Common Patterns + +### Defensive Null Checks in Interactions + +Always validate `CommandBuffer`, `Player`, and `ItemStack` before using them: + +```java +@Override +protected void firstRun(@Nonnull InteractionType type, + @Nonnull InteractionContext ctx, + @Nonnull CooldownHandler cooldown) { + CommandBuffer cb = ctx.getCommandBuffer(); + if (cb == null) { + ctx.getState().state = InteractionState.Failed; + return; + } + + Player player = cb.getComponent(ctx.getEntity(), Player.getComponentType()); + if (player == null) { + ctx.getState().state = InteractionState.Failed; + return; + } + + ItemStack held = ctx.getHeldItem(); + if (held == null) { + ctx.getState().state = InteractionState.Failed; + return; + } + + // Safe to proceed +} +``` + +### Looking Up Items at Runtime + +```java +// Validate an item ID string at runtime +public Item resolveItem(String itemId) { + var assetMap = Item.getAssetMap(); + if (assetMap == null) return null; + Item item = assetMap.getAsset(itemId); + if (item == null || item == Item.UNKNOWN) return null; + return item; +} +``` + +--- + +## Checklist: Creating a Custom Item + +1. [ ] Set `IncludesAssetPack: true` in `manifest.json` +2. [ ] Create item JSON in `Server/Item/Items/.json` +3. [ ] Create inventory icon in `Common/Icons/ItemsGenerated/` +4. [ ] Create 3D model in `Common/Items//model.blockymodel` +5. [ ] Create model texture in `Common/Items//model_texture.png` +6. [ ] (Optional) Add `"Recipe"` block for crafting +7. [ ] (Optional) Create interaction class extending `SimpleInstantInteraction` +8. [ ] (Optional) Register interaction in plugin `setup()` via `getCodecRegistry(Interaction.CODEC).register(...)` +9. [ ] (Optional) Link interaction in item JSON via `"Interactions"` block diff --git a/skills/hytale-logging/SKILL.md b/skills/hytale-logging/SKILL.md new file mode 100644 index 0000000..8247cc2 --- /dev/null +++ b/skills/hytale-logging/SKILL.md @@ -0,0 +1,191 @@ +--- +name: hytale-logging +description: Documents Hytale's HytaleLogger API for server-side logging in plugins. Use when adding log statements, configuring log levels, formatting log messages with printf-style arguments, or logging exceptions with stack traces. Triggers - log, logger, HytaleLogger, logging, atInfo, atWarning, atSevere, withCause, forEnclosingClass, log level, debug, server log. +--- + +# Hytale Logging Skill + +Use this skill when adding logging to Hytale plugins. Hytale provides `HytaleLogger`, a Flogger-based logger (`com.google.common.flogger.AbstractLogger`) that writes to the server's log file at `{Hytale install}/UserData/Saves/{World}/logs`. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **HytaleLogger** | Main logging API, extends `AbstractLogger` from Google Flogger | +| **Package** | `com.hypixel.hytale.logger.HytaleLogger` | +| **Log Levels** | `atInfo()`, `atWarning()`, `atSevere()` (default visible levels) | +| **Arguments** | `printf`-style format specifiers (`%s`, `%d`, `%f`, `%b`, `%c`) | +| **Exceptions** | `.withCause(exception)` to attach stack traces | +| **Backend** | `HytaleLoggerBackend` — manages log levels and sinks | + +--- + +## Creating a Logger + +Use `HytaleLogger.forEnclosingClass()` to create a logger scoped to the current class. Declare it as a `public static final` field so it can be reused across the plugin. + +```java +import com.hypixel.hytale.logger.HytaleLogger; + +public class ExamplePlugin extends JavaPlugin { + public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + @Override + protected void setup() { + LOGGER.atInfo().log("ExamplePlugin loaded"); + } +} +``` + +### Named Logger + +You can also create a logger with a custom name. The name appears before the log message in the log file for easier identification. + +```java +HytaleLogger LOGGER = HytaleLogger.get("MyPluginName"); +``` + +### Using the Logger from Other Classes + +Reference the static logger from your main plugin class: + +```java +ExamplePlugin.LOGGER.atInfo().log("hello world"); +``` + +--- + +## Log Levels + +Only `Info`, `Warning`, and `Severe` messages are printed with default configuration. + +| Method | Purpose | When to Use | +|--------|---------|-------------| +| `atInfo()` | Informational messages | Normal behavior, startup, state changes | +| `atWarning()` | Potential problems | Situations that could lead to errors | +| `atSevere()` | Serious errors | Failures that prevent correct operation | + +```java +LOGGER.atInfo().log("Provide high-level information about normal behavior."); +LOGGER.atWarning().log("Signal a potential problem or a situation that could lead to an error."); +LOGGER.atSevere().log("A serious error that will prevent things from working as expected."); +``` + +--- + +## Template Arguments (printf-style) + +HytaleLogger uses `printf`-style format specifiers. Pass arguments after the format string — do NOT use string concatenation. + +### Common Specifiers + +| Specifier | Type | Example | +|-----------|------|---------| +| `%s` | String | `"Hello %s", name` → `Hello World` | +| `%d` | Integer | `"Count: %d", 42` → `Count: 42` | +| `%f` | Float/Double | `"Value: %f", 3.14` → `Value: 3.140000` | +| `%b` | Boolean | `"Active: %b", true` → `Active: true` | +| `%c` | Char | `"Letter: %c", 'A'` → `Letter: A` | + +### Example + +```java +final String name = "World"; +LOGGER.atInfo().log("Hello %s", name); +// prints: Hello World + +int count = 5; +LOGGER.atInfo().log("Found %d items for player %s", count, playerName); +``` + +### Why printf-style over concatenation? + +- **Performance**: Arguments are only evaluated if the log level is active +- **Consistency**: Matches Flogger conventions +- **Safety**: Avoids null pointer issues with string concatenation + +--- + +## Exceptions and Causes + +Attach exceptions to log messages using `.withCause(exception)` to include the full stack trace. + +```java +try { + // risky operation +} catch (IOException e) { + LOGGER.atSevere().withCause(e).log("Failed to load configuration file"); +} +``` + +### Chaining with Arguments + +```java +try { + loadResource(path); +} catch (Exception e) { + LOGGER.atSevere().withCause(e).log("Failed to load resource: %s", path); +} +``` + +--- + +## Best Practices + +1. **Use `forEnclosingClass()`** — Automatically scopes the logger to the class; no manual name changes needed when refactoring. +2. **Prefer `atInfo()` for normal flow** — Reserve `atWarning()` and `atSevere()` for actual problems. +3. **Use printf-style arguments** — Never concatenate strings in log calls; arguments are lazily evaluated. +4. **Always attach exceptions** — Use `.withCause(e)` instead of logging `e.getMessage()` to preserve stack traces. +5. **Declare logger as `public static final`** — Allows reuse across the plugin and from other classes. +6. **Don't over-log** — Excessive `atInfo()` in tick loops will flood the log file and hurt performance. + +--- + +## Complete Example + +```java +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.pluginframework.JavaPlugin; + +public class MyPlugin extends JavaPlugin { + public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + @Override + protected void setup() { + LOGGER.atInfo().log("MyPlugin setup starting"); + + try { + initializeSystems(); + LOGGER.atInfo().log("MyPlugin setup complete — %d systems initialized", systemCount); + } catch (Exception e) { + LOGGER.atSevere().withCause(e).log("MyPlugin setup failed"); + } + } + + @Override + protected void start() { + LOGGER.atInfo().log("MyPlugin started"); + } + + private void onPlayerAction(String playerName, String action) { + LOGGER.atInfo().log("Player %s performed action: %s", playerName, action); + } +} +``` + +--- + +## Internal Details (Reference Only) + +| Class | Package | Role | +|-------|---------|------| +| `HytaleLogger` | `com.hypixel.hytale.logger` | Main logging API | +| `HytaleLoggerBackend` | `com.hypixel.hytale.logger.backend` | Backend, log levels, sinks | +| `HytaleFileHandler` | `com.hypixel.hytale.logger` | File-based log output | +| `HytaleLogManager` | `com.hypixel.hytale.logger` | Log manager integration | + +- `HytaleLogger.init()` — Initializes the logging backend (called during server boot). +- `HytaleLogger.replaceStd()` — Redirects `System.out` / `System.err` into logger streams. +- Log files are written to `{Hytale install}/UserData/Saves/{World}/logs`. diff --git a/skills/hytale-notifications/SKILL.md b/skills/hytale-notifications/SKILL.md new file mode 100644 index 0000000..1d2660c --- /dev/null +++ b/skills/hytale-notifications/SKILL.md @@ -0,0 +1,309 @@ +--- +name: hytale-notifications +description: Sends in-game notifications to players in Hytale plugins using NotificationUtil. Use when displaying item pickup-style notifications, toast messages, alert messages with icons, or any player notification with primary/secondary text. Triggers - notification, toast, alert, NotificationUtil, sendNotification, item pickup, player notification, message popup. +--- + +# Hytale Notifications Skill + +Use this skill when sending in-game notifications to players in Hytale plugins. Notifications appear similar to item pickup messages and consist of a primary message, secondary message, and an optional icon. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **NotificationUtil** | Utility class for sending notifications to players | +| **Primary Message** | Main `Message` displayed in the notification | +| **Secondary Message** | Additional `Message` displayed below the primary | +| **Icon** | An item icon shown on the left side of the notification | +| **PacketHandler** | Required to send notifications to a specific player | + +--- + +## Notification Structure + +A notification consists of three main components: + +1. **Primary Message**: The main `Message` displayed prominently in the notification +2. **Secondary Message**: Additional `Message` displayed below the primary message +3. **Icon**: An item icon that visually represents the notification, shown on the left side + +--- + +## Required Imports + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.Universe; +import com.hypixel.hytale.server.item.ItemStack; +import com.hypixel.hytale.server.item.ItemWithAllMetadata; +import com.hypixel.hytale.server.player.PlayerRef; +import com.hypixel.hytale.server.util.NotificationUtil; +``` + +--- + +## Sending Notifications + +Use the `NotificationUtil` class to send notifications to players. You need access to the `PacketHandler` of the player. + +### Getting PacketHandler + +The `PacketHandler` can be obtained from a `PlayerRef` using the `getPacketHandler()` method: + +```java +// From an event +PlayerRef playerRef = Universe.get().getPlayer(player.getUuid()); +var packetHandler = playerRef.getPacketHandler(); +``` + +### NotificationUtil.sendNotification() + +| Parameter | Type | Description | +|-----------|------|-------------| +| `packetHandler` | `PacketHandler` | The player's packet handler | +| `primaryMessage` | `Message` | Main notification text | +| `secondaryMessage` | `Message` | Secondary text below primary | +| `icon` | `ItemWithAllMetadata` | Item icon to display | + +--- + +## Basic Example + +```java +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.Universe; +import com.hypixel.hytale.server.event.player.PlayerReadyEvent; +import com.hypixel.hytale.server.item.ItemStack; +import com.hypixel.hytale.server.item.ItemWithAllMetadata; +import com.hypixel.hytale.server.util.NotificationUtil; + +public class NotificationExample { + + public static void onPlayerReady(PlayerReadyEvent event) { + var player = event.getPlayer(); + var playerRef = Universe.get().getPlayer(player.getUuid()); + var packetHandler = playerRef.getPacketHandler(); + + // Create messages + var primaryMessage = Message.raw("THIS WORKS!!!").color("#00FF00"); + var secondaryMessage = Message.raw("This is the secondary message").color("#228B22"); + + // Create icon from item + var icon = new ItemStack("Weapon_Sword_Mithril", 1).toPacket(); + + // Send notification + NotificationUtil.sendNotification( + packetHandler, + primaryMessage, + secondaryMessage, + (ItemWithAllMetadata) icon + ); + } +} +``` + +--- + +## Common Use Cases + +### Achievement/Quest Notification + +```java +public void sendAchievementNotification(PlayerRef playerRef, String achievement) { + var packetHandler = playerRef.getPacketHandler(); + + var primary = Message.raw("Achievement Unlocked!").color("#FFD700"); + var secondary = Message.raw(achievement).color("#FFFFFF"); + var icon = new ItemStack("Item_Trophy_Gold", 1).toPacket(); + + NotificationUtil.sendNotification( + packetHandler, + primary, + secondary, + (ItemWithAllMetadata) icon + ); +} +``` + +### Item Received Notification + +```java +public void sendItemReceivedNotification(PlayerRef playerRef, String itemId, int quantity) { + var packetHandler = playerRef.getPacketHandler(); + + var primary = Message.raw("Item Received").color("#00FF00"); + var secondary = Message.raw("+" + quantity + " " + itemId).color("#AAAAAA"); + var icon = new ItemStack(itemId, quantity).toPacket(); + + NotificationUtil.sendNotification( + packetHandler, + primary, + secondary, + (ItemWithAllMetadata) icon + ); +} +``` + +### Warning/Alert Notification + +```java +public void sendWarningNotification(PlayerRef playerRef, String warning) { + var packetHandler = playerRef.getPacketHandler(); + + var primary = Message.raw("Warning!").color("#FF0000"); + var secondary = Message.raw(warning).color("#FFAAAA"); + var icon = new ItemStack("Item_Warning_Sign", 1).toPacket(); + + NotificationUtil.sendNotification( + packetHandler, + primary, + secondary, + (ItemWithAllMetadata) icon + ); +} +``` + +### Level Up Notification + +```java +public void sendLevelUpNotification(PlayerRef playerRef, int newLevel) { + var packetHandler = playerRef.getPacketHandler(); + + var primary = Message.raw("LEVEL UP!").color("#FFD700"); + var secondary = Message.raw("You are now level " + newLevel).color("#FFFFFF"); + var icon = new ItemStack("Item_Star", 1).toPacket(); + + NotificationUtil.sendNotification( + packetHandler, + primary, + secondary, + (ItemWithAllMetadata) icon + ); +} +``` + +--- + +## Message Styling + +Notifications use the standard `Message` API for styling: + +| Method | Description | +|--------|-------------| +| `Message.raw(String)` | Create message from raw text | +| `.color(String)` | Apply hex color (e.g., `"#00FF00"`) | +| `.color(Color)` | Apply named color constant | +| `Message.join(Message...)` | Join multiple messages | + +### Hex Color Examples + +```java +// Green success +Message.raw("Success!").color("#00FF00"); + +// Gold achievement +Message.raw("Achievement").color("#FFD700"); + +// Red warning +Message.raw("Warning").color("#FF0000"); + +// Blue info +Message.raw("Info").color("#0088FF"); +``` + +--- + +## Creating Icons + +Icons are created from `ItemStack` objects converted to packet format: + +```java +// Create from item ID and quantity +var icon = new ItemStack("Weapon_Sword_Mithril", 1).toPacket(); + +// Cast to ItemWithAllMetadata for sendNotification +NotificationUtil.sendNotification( + packetHandler, + primary, + secondary, + (ItemWithAllMetadata) icon +); +``` + +### Common Icon Items + +| Item ID | Use Case | +|---------|----------| +| `Weapon_Sword_*` | Combat notifications | +| `Item_Trophy_*` | Achievement notifications | +| `Item_Coin_*` | Currency notifications | +| `Food_*` | Food/buff notifications | +| `Tool_*` | Tool-related notifications | + +--- + +## Utility Wrapper Class + +Consider creating a utility wrapper for consistent notification styling: + +```java +public class Notifications { + + public static void success(PlayerRef player, String title, String message, String iconId) { + send(player, title, "#00FF00", message, "#AAFFAA", iconId); + } + + public static void warning(PlayerRef player, String title, String message, String iconId) { + send(player, title, "#FFAA00", message, "#FFDDAA", iconId); + } + + public static void error(PlayerRef player, String title, String message, String iconId) { + send(player, title, "#FF0000", message, "#FFAAAA", iconId); + } + + public static void info(PlayerRef player, String title, String message, String iconId) { + send(player, title, "#0088FF", message, "#AADDFF", iconId); + } + + private static void send(PlayerRef player, String title, String titleColor, + String message, String messageColor, String iconId) { + var packetHandler = player.getPacketHandler(); + var primary = Message.raw(title).color(titleColor); + var secondary = Message.raw(message).color(messageColor); + var icon = new ItemStack(iconId, 1).toPacket(); + + NotificationUtil.sendNotification( + packetHandler, + primary, + secondary, + (ItemWithAllMetadata) icon + ); + } +} +``` + +--- + +## Best Practices + +1. **Keep messages concise**: Notifications are meant for quick information +2. **Use appropriate colors**: Green for success, red for errors, gold for achievements +3. **Choose relevant icons**: Match the icon to the notification context +4. **Avoid spamming**: Don't send too many notifications in quick succession +5. **Localize text**: Use translation keys for user-facing notification text + +--- + +## Related APIs + +- [Chat Formatting](../hytale-chat-formatting/SKILL.md) - For styled chat messages +- [Message API](#message-styling) - Core message styling +- [PlayerRef](https://hytalemodding.dev/en/docs/server/entities) - Player reference access + +--- + +## References + +- [Official Documentation](https://hytalemodding.dev/en/docs/guides/plugin/send-notifications) diff --git a/skills/hytale-npc-templates/SKILL.md b/skills/hytale-npc-templates/SKILL.md new file mode 100644 index 0000000..7eb5715 --- /dev/null +++ b/skills/hytale-npc-templates/SKILL.md @@ -0,0 +1,1240 @@ +--- +name: hytale-npc-templates +description: Documents Hytale's JSON-based NPC template and behavior system for defining NPC AI via data-driven templates. Covers template structure, variants, states, substates, sensors, actions, motions, state transitions, components, detection (sight/hearing), combat (melee attacks, chaining), inter-NPC interaction (beacons), leashing, searching, and reusable instruction components. Use when creating NPC behavior, defining NPC templates, adding NPC states, configuring NPC detection/combat, or building reusable NPC components. Triggers - NPC template, NPC behavior, NPC state, NPC sensor, NPC action, NPC motion, state transition, NPC combat, NPC detection, NPC component, Template_, Variant, BlankTemplate, Instructions, StartState, Random action, Timeout, PlayAnimation, StateTransitions, Component_Instruction, Component_Sensor, Intelligent_Chase, Soft_Leash, Standard_Detection, Damage_Check, beacon, NPC group, AttitudeGroup, DefaultPlayerAttitude, InteractionVars, melee attack, attack chaining, Root Interaction. +--- + +# Hytale NPC Template & Behavior System + +Use this skill when defining NPC behavior through JSON templates. This covers the data-driven side of NPC creation — how NPCs think, act, detect threats, fight, interact with other NPCs, and transition between behavioral states. For programmatic NPC spawning via Java, see the `hytale-spawning-npcs` skill instead. + +> **Prerequisite:** Familiarity with JSON asset structure under `Server/` directories and the Hytale ECS architecture. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **Template** | Abstract JSON file defining base NPC behavior, parameters, and states | +| **Variant** | Concrete NPC role that extends a template with specific parameter overrides | +| **State** | A top-level behavioral mode (e.g., `Idle`, `Sleep`, `Combat`) | +| **Substate** | A state nested within another, prefixed with `.` (e.g., `.Default`, `.Guard`) | +| **Sensor** | Condition that gates instruction execution (e.g., `State`, `Target`, `Beacon`, `Mob`, `Damage`) | +| **Action** | Operations performed when sensor conditions are met (e.g., `State`, `Timeout`, `Random`, `Attack`) | +| **Motion** | Movement behavior (e.g., `Seek`, `Wander`, `Nothing`, `Follow_Path`) | +| **StateTransition** | Actions performed sequentially when transitioning between states (e.g., animations) | +| **Component** | Reusable instruction/sensor module referenced via `"Reference"` | +| **Parameter** | Configurable value exposed in the template's `Parameters` block | +| **Beacon** | Message-based inter-NPC communication system | +| **NPC Group** | Named set of NPC roles used for filtering (attitudes, beacons, food targets) | +| **Attitude Group** | Defines Friendly/Hostile/Neutral relationships between NPC groups | + +--- + +## File Locations + +| File Type | Path | +|-----------|------| +| NPC Templates | `Server/NPC/Templates/Template_.json` | +| NPC Variants (Roles) | `Server/NPC/Roles/.json` | +| NPC Components | `Server/NPC/Components/Component__.json` | +| NPC Groups | `Server/NPC/Groups/.json` | +| Attitude Groups | `Server/NPC/AttitudeGroups/.json` | +| Appearance files | `Server/NPC/Appearances/.json` | +| Root Interactions | `Server/Item/RootInteractions/Root_NPC_.json` | +| Attack Interactions | `Server/Item/Interactions/.json` | +| Spawn Beacons | `Server/NPC/SpawnBeacons/.json` | + +--- + +## Template Structure + +### Blank Template (Starting Point) + +Always start from `BlankTemplate` and customize. A template is `"Type": "Abstract"` and defines defaults through `Parameters`. + +```json +{ + "Type": "Abstract", + "Parameters": { + "Appearance": { + "Value": "Bear_Grizzly", + "Description": "Model to be used" + }, + "DropList": { + "Value": "Empty", + "Description": "Drop Items" + }, + "MaxHealth": { + "Value": 100, + "Description": "Max health for the NPC" + }, + "NameTranslationKey": { + "Value": "server.npcRoles.Template.name", + "Description": "Translation key for NPC name display" + } + }, + "Appearance": { "Compute": "Appearance" }, + "DropList": { "Compute": "DropList" }, + "MaxHealth": { "Compute": "MaxHealth" }, + "MotionControllerList": [ + { + "Type": "Walk", + "MaxWalkSpeed": 3, + "Gravity": 10, + "MaxFallSpeed": 8, + "Acceleration": 10 + } + ], + "Instructions": [ + { + "Sensor": { + "Type": "Any" + }, + "BodyMotion": { + "Type": "Nothing" + } + } + ], + "NameTranslationKey": { "Compute": "NameTranslationKey" } +} +``` + +### Variant (Role) File + +A variant extends a template with concrete parameter values. Place next to the template. + +```json +{ + "Type": "Variant", + "Reference": "Template_Goblin_Ogre", + "Modify": { + "Appearance": "Goblin", + "MaxHealth": 124 + } +} +``` + +Variants can also override `InteractionVars`, `Parameters`, and `NameTranslationKey`. + +--- + +## Parameters + +Parameters are defined in the `"Parameters"` block and referenced via `{ "Compute": "ParamName" }`. They support computed expressions like `"Compute": "ViewRange / DistractedPenalty"`. + +```json +"Parameters": { + "Appearance": { + "Value": "Bear_Grizzly", + "Description": "Model to be used" + }, + "ViewRange": { + "Value": 15, + "Description": "View range in blocks" + }, + "DistractedPenalty": { + "Value": 2, + "Description": "Factor by which view/hearing range is divided when distracted" + } +} +``` + +**Computed expressions:** `{ "Compute": "ViewRange / DistractedPenalty" }` divides ViewRange by DistractedPenalty at runtime. + +--- + +## States & Substates + +### Setting the Start State + +```json +"StartState": "Idle", +``` + +### Top-Level States + +Top-level states are behavioral modes like `Idle`, `Sleep`, `Eat`, `Combat`, `Alerted`, `ReturnHome`, `Search`. + +```json +"Instructions": [ + { + "Sensor": { "Type": "State", "State": "Idle" }, + "Instructions": [ ... ] + }, + { + "Sensor": { "Type": "State", "State": "Sleep" }, + "Instructions": [ ... ] + }, + { + "Sensor": { "Type": "State", "State": "Combat" }, + "Instructions": [ ... ] + } +] +``` + +### Substates + +Substates are nested within a parent state and prefixed with `.`. The `.Default` substate is used automatically when entering the parent state. + +```json +{ + "Sensor": { "Type": "State", "State": "Idle" }, + "Instructions": [ + { + "Sensor": { "Type": "State", "State": ".Default" }, + "Instructions": [ ... ] + }, + { + "Sensor": { "Type": "State", "State": ".Guard" }, + "Instructions": [ ... ] + } + ] +} +``` + +### Switching States + +Use a `State` action to switch: + +```json +{ "Type": "State", "State": "Combat" } +``` + +For substates: + +```json +{ "Type": "State", "State": ".Guard" } +``` + +--- + +## Sensors + +Sensors are conditions that gate instruction execution. + +| Sensor Type | Description | Key Fields | +|------------|-------------|------------| +| `State` | Matches current NPC state | `State` | +| `Any` | Always matches | `Once` (execute only once) | +| `Target` | Detects locked/nearby target | `Range`, `TargetSlot`, `Filters` | +| `Mob` | Detects nearby NPCs | `Range`, `Filters` ([`NPCGroup`, `LineOfSight`]) | +| `Beacon` | Listens for inter-NPC messages | `Message`, `Range`, `TargetSlot` | +| `Damage` | Reacts to incoming damage | `Combat`, `TargetSlot` | +| `Leash` | Checks distance from spawn | `Range` | +| `And` | Combines multiple sensors | `Sensors` (array) | +| `Reference` | Uses a reusable sensor component | Component name | + +### Sensor with Filters + +```json +{ + "Sensor": { + "Type": "Target", + "Range": { "Compute": "AttackDistance" }, + "Filters": [ + { "Type": "LineOfSight" } + ] + }, + "Actions": [ ... ] +} +``` + +### Mob Sensor (NPC Group Filtering) + +```json +{ + "Sensor": { + "Type": "Mob", + "Range": 2.5, + "Filters": [ + { "Type": "NPCGroup", "IncludeGroups": { "Compute": "FoodNPCGroups" } }, + { "Type": "LineOfSight" } + ] + } +} +``` + +--- + +## Actions + +Actions are operations executed when sensor conditions are met. + +| Action Type | Description | Key Fields | +|------------|-------------|------------| +| `State` | Switch to a different state | `State` | +| `ParentState` | Switch using imported state name | `State` (from `_ImportStates`) | +| `Random` | Randomly pick a weighted action | `Actions` (array with `Weight` + `Action`) | +| `Timeout` | Wait for a duration | `Delay` (`[min, max]` or fixed) | +| `PlayAnimation` | Play an animation | `Slot`, `Animation` | +| `Attack` | Execute an attack interaction | `Attack`, `AttackPauseRange` | +| `Inventory` | Manipulate NPC inventory | `Operation`, `Item`, `Slot`, `UseTarget` | +| `Beacon` | Send message to nearby NPCs | `Message`, `TargetGroups`, `SendTargetSlot` | +| `TriggerSpawnBeacon` | Trigger a manual spawn beacon | `BeaconSpawn`, `Range` | +| `SetStat` | Set an entity stat | `Stat`, `Value` | +| `Remove` | Remove the target entity | — | +| `Despawn` | Despawn this NPC | — | +| `Sequence` | Execute multiple actions in same tick | `Actions` (array) | + +### Random Action (Weighted State Selection) + +```json +{ + "Actions": [ + { + "Type": "Random", + "Actions": [ + { "Weight": 70, "Action": { "Type": "State", "State": ".Guard" } }, + { "Weight": 20, "Action": { "Type": "State", "State": "Sleep" } }, + { "Weight": 10, "Action": { "Type": "State", "State": "Eat" } } + ] + } + ] +} +``` + +### Timeout with State Switch + +```json +{ + "Continue": true, + "ActionsBlocking": true, + "Actions": [ + { "Type": "Timeout", "Delay": [15, 30] }, + { "Type": "State", "State": ".Default" } + ] +} +``` + +### Inventory Actions + +| Operation | Description | +|-----------|-------------| +| `SetHotbar` | Place an item in a hotbar slot | +| `EquipHotbar` | Switch active hotbar slot | + +```json +{ + "Type": "Inventory", + "Operation": "SetHotbar", + "Item": { "Compute": "EatItem" }, + "Slot": 2, + "UseTarget": false +} +``` + +> **`UseTarget: false`** is required to act on the NPC itself, not its target. + +--- + +## Instruction Flags + +| Flag | Description | +|------|-------------| +| `Continue` | If `true`, continue evaluating subsequent instructions even if this one matches | +| `ActionsBlocking` | If `true`, wait for all actions to complete before proceeding | +| `Once` | (On sensors) Execute only once when first entering the state | + +**Common pattern** — timeout then switch state: + +```json +{ + "Continue": true, + "ActionsBlocking": true, + "Actions": [ + { "Type": "Timeout", "Delay": [5, 10] }, + { "Type": "State", "State": "Idle" } + ] +} +``` + +--- + +## Motions + +Motions control NPC movement. Set via `BodyMotion` or `HeadMotion` on instructions. + +| Motion Type | Description | Key Fields | +|------------|-------------|------------| +| `Nothing` | Stand still | — | +| `Seek` | Move toward target/position | `SlowDownDistance`, `StopDistance`, `RelativeSpeed`, `UsePathfinder` | +| `Wander` | Random wandering | `MaxHeadingChange`, `RelativeSpeed` | +| `WanderInCircle` | Circular wandering | `Radius`, `MaxHeadingChange`, `RelativeSpeed` | +| `Watch` | Look at target (head only) | — | +| `Aim` | Aim at target (combat) | `RelativeTurnSpeed` | +| `Sequence` | Chain motions | `Motions` (array), `Looped` | +| `Timer` | Motion for a duration | `Time`, `Motion` | + +### MotionControllerList + +Defined at template level: + +```json +"MotionControllerList": [ + { + "Type": "Walk", + "MaxWalkSpeed": 3, + "Gravity": 10, + "MaxFallSpeed": 8, + "Acceleration": 10 + } +] +``` + +### Head Motion (Watch Target) + +```json +{ + "Continue": true, + "Sensor": { "Type": "Target", "Range": { "Compute": "AlertedRange" } }, + "HeadMotion": { "Type": "Watch" } +} +``` + +### Body Motion with Pathfinding + +```json +{ + "Sensor": { "Type": "Leash", "Range": { "Compute": "LeashDistance * 0.3" } }, + "BodyMotion": { + "Type": "Seek", + "SlowDownDistance": { "Compute": "LeashDistance * 0.4" }, + "StopDistance": { "Compute": "LeashDistance * 0.2" }, + "RelativeSpeed": 0.8, + "UsePathfinder": true + } +} +``` + +### Search Wander Pattern + +```json +"BodyMotion": { + "Type": "Sequence", + "Motions": [ + { + "Type": "Timer", + "Time": [3, 6], + "Motion": { "Type": "Wander", "MaxHeadingChange": 1, "RelativeSpeed": 0.5 } + }, + { + "Type": "Sequence", + "Looped": true, + "Motions": [ + { + "Type": "Timer", + "Time": [3, 6], + "Motion": { "Type": "WanderInCircle", "Radius": 10, "MaxHeadingChange": 60, "RelativeSpeed": 0.5 } + }, + { + "Type": "Timer", + "Time": [2, 3], + "Motion": { "Type": "Nothing" } + } + ] + } + ] +} +``` + +--- + +## State Transitions + +State transitions define actions executed **sequentially** when switching between states. Defined in `"StateTransitions"` at the template level (above `"Instructions"`). + +An empty `"From"` or `"To"` array means **all states**. + +```json +"StateTransitions": [ + { + "States": [ + { "From": ["Idle"], "To": ["Sleep"] } + ], + "Actions": [ + { "Type": "PlayAnimation", "Slot": "Status", "Animation": "Laydown" }, + { "Type": "Timeout", "Delay": [1, 1] } + ] + }, + { + "States": [ + { "From": ["Sleep"], "To": [] } + ], + "Actions": [ + { "Type": "PlayAnimation", "Slot": "Status", "Animation": "Wake" }, + { "Type": "Timeout", "Delay": [1, 1] } + ] + } +] +``` + +### Inventory State Transitions (Equip/Unequip) + +```json +{ + "States": [ + { "From": ["Idle"], "To": ["Eat"] } + ], + "Actions": [ + { "Type": "Inventory", "Operation": "SetHotbar", "Item": { "Compute": "EatItem" }, "Slot": 2, "UseTarget": false }, + { "Type": "Inventory", "Operation": "EquipHotbar", "Slot": 2, "UseTarget": false } + ] +} +``` + +### Combat Entry Transition (Warn Allies) + +```json +{ + "States": [ + { "From": [], "To": ["Combat"] } + ], + "Actions": [ + { "Type": "PlayAnimation", "Slot": "Status" }, + { "Type": "Beacon", "Message": "Goblin_Ogre_Warn", "TargetGroups": { "Compute": "WarnGroups" }, "SendTargetSlot": "LockedTarget" } + ] +} +``` + +--- + +## Reusable Components + +Components are reusable chunks of instruction/sensor logic. They use `"Type": "Component"` and expose parameters. + +### Component Structure + +```json +{ + "Type": "Component", + "Class": "Instruction", + "Parameters": { + "_ImportStates": ["Main"], + "Animation": { + "Value": "", + "Description": "The animation to play" + }, + "Duration": { + "Value": [3, 5], + "Description": "The amount of time to wait before transitioning" + } + }, + "Content": { + "Continue": true, + "Instructions": [ + { + "Reference": "Component_Instruction_State_Timeout", + "Modify": { + "_ExportStates": ["Main"], + "Delay": { "Compute": "Duration" } + } + }, + { + "Reference": "Component_Instruction_Play_Animation", + "Modify": { + "Animation": { "Compute": "Animation" } + } + } + ] + } +} +``` + +### Using Components (Reference + Modify) + +```json +{ + "Reference": "Component_Instruction_Intelligent_Idle_Motion_Follow_Path" +} +``` + +With parameter overrides: + +```json +{ + "Reference": "Component_Instruction_State_Timeout", + "Modify": { + "_ExportStates": ["Idle.Default"], + "Delay": [30, 45] + } +} +``` + +### State Import/Export Pattern + +- `_ImportStates`: Declares named state slots a component expects from the user. +- `_ExportStates`: Provides concrete state names to fill those slots. +- `ParentState` action: Uses the imported state name to switch states. + +```json +// In component: +"_ImportStates": ["Main"], +"Actions": [ + { "Type": "ParentState", "State": "Main" } +] + +// When referencing: +"Modify": { + "_ExportStates": ["Idle.Default"] +} +``` + +### Common Built-In Components + +| Component | Class | Purpose | +|-----------|-------|---------| +| `Component_Instruction_Intelligent_Idle_Motion_Follow_Path` | Instruction | Follow a path marker for idle guard behavior | +| `Component_Instruction_Intelligent_Chase` | Instruction | Smart chase behavior with pathfinding and lost-target handling | +| `Component_Instruction_Soft_Leash` | Instruction | Return home if NPC goes too far from spawn | +| `Component_Instruction_Damage_Check` | Instruction | React to incoming damage | +| `Component_Instruction_Play_Animation` | Instruction | Play a named animation | +| `Component_Instruction_State_Timeout` | Instruction | Wait then switch to a parent state | +| `Component_Instruction_Play_Animation_In_State_For_Duration` | Instruction | Play animation for a random duration then switch state | +| `Component_Sensor_Standard_Detection` | Sensor | Sight + hearing detection with attitude filtering | +| `Component_Sensor_Lost_Target_Detection` | Sensor | Detect a previously-seen target | + +--- + +## Detection System + +### Standard Detection Sensor + +Handles sight (view range + cone + line of sight) and hearing (range-based, ignores crouching/still targets, blocked by walls). + +```json +{ + "Sensor": { + "Reference": "Component_Sensor_Standard_Detection", + "Modify": { + "ViewRange": { "Compute": "ViewRange" }, + "ViewSector": { "Compute": "ViewSector" }, + "HearingRange": { "Compute": "HearingRange" }, + "ThroughWalls": false, + "AbsoluteDetectionRange": { "Compute": "AbsoluteDetectionRange" }, + "Attitudes": ["Hostile"] + } + }, + "Actions": [ + { "Type": "State", "State": "Alerted" } + ] +} +``` + +**Detection order:** +1. **Absolute detection range** — guaranteed detection within this radius +2. **View range/sector** — line-of-sight check within the view cone +3. **Hearing range** — detects walking/running (non-crouching) targets, blocked by walls + +### Reduced Detection (Distracted States) + +Divide ranges by a penalty factor for sleeping/eating states: + +```json +"ViewRange": { "Compute": "ViewRange / DistractedPenalty" }, +"HearingRange": { "Compute": "ViewRange / DistractedPenalty" } +``` + +### Damage Check Component + +Detects incoming damage and transitions to combat/alert: + +```json +{ + "Reference": "Component_Instruction_Damage_Check", + "Modify": { + "_ExportStates": ["Alerted", "Alerted"], + "AlertedRange": { "Compute": "AlertedRange" } + } +} +``` + +### Attitude Groups + +Define NPC relationship groups: + +```json +{ + "Groups": { + "Friendly": ["Goblin"], + "Hostile": [] + } +} +``` + +### Template Attitude Configuration + +```json +"DefaultPlayerAttitude": "Hostile", +"DefaultNPCAttitude": "Ignore", +"AttitudeGroup": { "Compute": "AttitudeGroup" } +``` + +--- + +## Combat System + +### Alerted State Pattern + +Transitional state between detection and combat: + +```json +{ + "Sensor": { "Type": "State", "State": "Alerted" }, + "Instructions": [ + { + "Reference": "Component_Instruction_Play_Animation", + "Modify": { "Animation": "Alerted" } + }, + { + "Continue": true, + "Sensor": { + "Type": "Target", + "Range": { "Compute": "AlertedRange" }, + "Filters": [ { "Type": "LineOfSight" } ] + }, + "HeadMotion": { "Type": "Watch" } + }, + { + "Sensor": { "Type": "Target", "Range": { "Compute": "AlertedRange" } }, + "ActionsBlocking": true, + "Actions": [ + { "Type": "Timeout", "Delay": [1, 1] }, + { "Type": "State", "State": "Combat" } + ] + }, + { + "Actions": [ { "Type": "State", "State": "Idle" } ] + } + ] +} +``` + +### Combat State with Chase Substate + +```json +{ + "Sensor": { "Type": "State", "State": "Combat" }, + "Instructions": [ + { + "Sensor": { "Type": "State", "State": ".Chase" }, + "Instructions": [ + { + "Sensor": { + "Type": "Target", + "Range": { "Compute": "AttackDistance" }, + "Filters": [ { "Type": "LineOfSight" } ] + }, + "Actions": [ { "Type": "State", "State": ".Default" } ] + }, + { + "Reference": "Component_Instruction_Soft_Leash", + "Modify": { + "_ExportStates": ["ReturnHome"], + "LeashDistance": { "Compute": "LeashDistance" }, + "LeashMinPlayerDistance": { "Compute": "LeashMinPlayerDistance" }, + "LeashTimer": { "Compute": "LeashTimer" }, + "HardLeashDistance": { "Compute": "HardLeashDistance" } + } + }, + { + "Reference": "Component_Instruction_Intelligent_Chase", + "Modify": { + "_ExportStates": ["Search", "Search", "ReturnHome"], + "ViewRange": { "Compute": "AlertedRange * 2" }, + "HearingRange": { "Compute": "HearingRange * 2" }, + "StopDistance": 0.1, + "RelativeSpeed": 0.5 + } + } + ] + }, + { + "$Comment": "NPC melee attack", + "Sensor": { + "Type": "Target", + "Range": { "Compute": "AttackDistance" }, + "Filters": [ { "Type": "LineOfSight" } ], + "ActionsBlocking": true, + "Actions": [ + { + "Type": "Attack", + "Attack": { "Compute": "Attack" }, + "AttackPauseRange": { "Compute": "AttackPauseRange" } + }, + { "Type": "Timeout", "Delay": [0.2, 0.2] } + ], + "HeadMotion": { + "Type": "Aim", + "RelativeTurnSpeed": { "Compute": "CombatRelativeTurnSpeed" } + } + }, + "Actions": [ { "Type": "State", "State": ".Chase" } ] + } + ] +} +``` + +### Key Combat Parameters + +```json +"Attack": { + "Value": "Root_NPC_Goblin_Ogre_Attack", + "Description": "The attack to use." +}, +"AttackDistance": { + "Value": 2, + "Description": "The distance at which an NPC will execute attacks" +}, +"AttackPauseRange": { + "Value": [1.5, 2], + "Description": "Absolute minimum time before a second attack" +}, +"CombatRelativeTurnSpeed": { + "Value": 1.5, + "Description": "Turn speed modifier in combat" +}, +"LeashDistance": { + "Value": 20, + "Description": "Range after which NPC starts wanting to return" +}, +"HardLeashDistance": { + "Value": 60, + "Description": "Absolute maximum from leash position" +} +``` + +--- + +## Attack Interactions + +### Root Interaction (Chaining Attacks) + +```json +{ + "Interactions": [ + { + "Type": "Chaining", + "ChainId": "Slashes", + "ChainingAllowance": 15, + "Next": [ + "Goblin_Ogre_Swing_Left", + "Goblin_Ogre_Swing_Right", + "Goblin_Ogre_Swing_Down" + ] + } + ], + "Tags": { + "Attack": ["Melee"] + } +} +``` + +NPCs attack in sequence: first `Swing_Left`, then `Swing_Right` (if within 15s), then `Swing_Down`. + +### Individual Attack Interaction + +```json +{ + "Type": "Simple", + "Effects": { + "ItemPlayerAnimationsId": "Goblin_Club", + "ItemAnimationId": "SwingLeft" + }, + "RunTime": 0.2, + "Next": { + "Type": "Selector", + "RunTime": 0.25, + "Selector": { + "Id": "Horizontal", + "Direction": "ToLeft", + "TestLineOfSight": true, + "ExtendTop": 0.5, + "ExtendBottom": 2, + "StartDistance": 0.1, + "EndDistance": 3.5, + "Length": 60, + "RollOffset": 0, + "YawStartOffset": -30 + }, + "HitEntity": { + "Interactions": [ + { + "Parent": "DamageEntityParent", + "DamageCalculator": { + "BaseDamage": { "Physical": 8 }, + "RandomPercentageModifier": 0.1 + }, + "DamageEffects": { + "Knockback": { "Force": 0.5, "RelativeX": -5, "RelativeZ": -5, "VelocityY": 5 }, + "WorldSoundEventId": "SFX_Unarmed_Impact", + "WorldParticles": [ { "SystemId": "Impact_Blade_01" } ] + } + } + ] + }, + "Next": { + "Type": "Simple", + "RunTime": 0.1 + } + } +} +``` + +### InteractionVars (Template-Level Damage Override) + +Templates can define overridable interaction variable slots: + +```json +"InteractionVars": { + "Melee_Damage": { + "Interactions": [ + { + "Parent": "NPC_Attack_Melee_Damage", + "DamageCalculator": { + "Type": "Absolute", + "BaseDamage": { "Physical": 10 }, + "RandomPercentageModifier": 0.1 + } + } + ] + } +} +``` + +Variants override these via `"Modify"`: + +```json +"InteractionVars": { + "Melee_SwingDown_Damage": { + "Interactions": [ + { + "Parent": "Goblin_Ogre_Swing_Down_Damage", + "DamageCalculator": { + "Type": "Absolute", + "BaseDamage": { "Physical": 20 } + } + } + ] + } +} +``` + +--- + +## Inter-NPC Interaction + +### Beacon Communication + +NPCs communicate via named beacon messages. One NPC sends a message, another listens for it. + +**Listening for a beacon (receiver):** + +```json +{ + "Sensor": { + "Type": "Beacon", + "Message": "Annoy_Ogre", + "Range": 5 + }, + "Actions": [ + { + "Type": "Attack", + "Attack": { "Compute": "SleepingAttack" }, + "AttackPauseRange": [1, 2] + } + ] +} +``` + +**Sending a beacon (via state transition):** + +```json +{ + "Type": "Beacon", + "Message": "Goblin_Ogre_Warn", + "TargetGroups": { "Compute": "WarnGroups" }, + "SendTargetSlot": "LockedTarget" +} +``` + +### NPC Groups + +Define groups for filtering: + +```json +{ + "IncludeRoles": ["Goblin_Scrapper"] +} +``` + +```json +{ + "IncludeRoles": ["Edible_Rat"] +} +``` + +### Spawn Beacons (Manual NPC Spawning) + +Trigger beacon-based NPC spawning for inter-NPC behaviors: + +**Spawn beacon definition:** + +```json +{ + "Environments": [], + "NPCs": [ + { "Weight": 1, "Id": "Edible Rat" } + ], + "SpawnAfterGameTimeRange": ["PT5M", "PT10M"], + "NPCSpawnState": "Seek", + "TargetSlot": "LockedTarget" +} +``` + +**Triggering from template:** + +```json +{ + "Continue": true, + "Sensor": { "Type": "Any", "Once": true }, + "Actions": [ + { + "Type": "TriggerSpawnBeacon", + "BeaconSpawn": { "Compute": "FoodNPCBeacon" }, + "Range": 15 + } + ] +} +``` + +### Edible Critter Template Pattern + +Generic template for NPCs that seek a target and get consumed: + +```json +{ + "Type": "Abstract", + "KnockbackScale": 0.5, + "Parameters": { + "Appearance": { "Value": "Rat", "Description": "Model to be used" }, + "WalkSpeed": { "Value": 3, "Description": "How fast this critter moves" }, + "SeekRange": { "Value": 40, "Description": "How far it can be from eater" }, + "MaxHealth": { "Value": 100, "Description": "Max health for the NPC" } + }, + "Appearance": { "Compute": "Appearance" }, + "StartState": "Idle", + "MaxHealth": { "Compute": "MaxHealth" }, + "Instructions": [ + { + "Instructions": [ + { + "Sensor": { "Type": "State", "State": "Idle" }, + "Instructions": [ + { + "Sensor": { "Type": "Beacon", "Message": "Approach_Target", "TargetSlot": "LockedTarget" }, + "Actions": [ { "Type": "State", "State": "Seek" } ] + }, + { + "ActionsBlocking": true, + "Actions": [ + { "Type": "Timeout", "Delay": [1, 1] }, + { "Type": "Despawn" } + ] + } + ] + }, + { + "Sensor": { "Type": "State", "State": "Seek" }, + "Instructions": [ + { + "Sensor": { "Type": "Target", "TargetSlot": "LockedTarget", "Range": { "Compute": "SeekRange" } }, + "BodyMotion": { "Type": "Seek", "SlowDownDistance": 0.1, "StopDistance": 0.1 } + }, + { + "ActionsBlocking": true, + "Actions": [ + { "Type": "Timeout", "Delay": [1, 1] }, + { "Type": "State", "State": "Idle" } + ] + } + ] + } + ] + } + ] +} +``` + +--- + +## ReturnHome & Search States + +### ReturnHome + +Handles returning to spawn point after leash triggers. Heals to full on arrival. + +```json +{ + "Sensor": { "Type": "State", "State": "ReturnHome" }, + "Instructions": [ + { + "Sensor": { + "Type": "And", + "Sensors": [ + { "Type": "Damage", "Combat": true, "TargetSlot": "LockedTarget", + "Enabled": { "Compute": "AbsoluteDetectionRange > 0" } }, + { "Type": "Target", "TargetSlot": "LockedTarget", + "Range": { "Compute": "AbsoluteDetectionRange" } } + ] + }, + "Actions": [ { "Type": "State", "State": "Combat" } ] + }, + { + "Sensor": { "Type": "Leash", "Range": { "Compute": "LeashDistance * 0.3" } }, + "BodyMotion": { + "Type": "Seek", + "SlowDownDistance": { "Compute": "LeashDistance * 0.4" }, + "StopDistance": { "Compute": "LeashDistance * 0.2" }, + "RelativeSpeed": 0.8, + "UsePathfinder": true + } + }, + { + "Actions": [ + { "Type": "SetStat", "Stat": "Health", "Value": 1000000 }, + { "Type": "State", "State": "Idle" } + ] + } + ] +} +``` + +### Search State + +Wander around looking for lost target before returning to idle: + +```json +{ + "Sensor": { "Type": "State", "State": "Search" }, + "Instructions": [ + { + "Sensor": { "Type": "Damage", "Combat": true, "TargetSlot": "LockedTarget" }, + "Actions": [ { "Type": "State", "State": "Alerted" } ] + }, + { + "Instructions": [ + { + "Sensor": { "Reference": "Component_Sensor_Lost_Target_Detection", "Modify": { ... } }, + "Actions": [ { "Type": "State", "State": "Combat" } ] + }, + { + "Sensor": { "Reference": "Component_Sensor_Standard_Detection", "Modify": { ... } }, + "Actions": [ { "Type": "State", "State": "Alerted" } ] + }, + { + "BodyMotion": { "Type": "Sequence", "Motions": [ /* wander pattern */ ] }, + "ActionsBlocking": true, + "Actions": [ + { "Type": "Timeout", "Delay": [4, 5] }, + { "Type": "State", "State": "Idle" } + ] + } + ] + } + ] +} +``` + +--- + +## Debugging + +Add to the top of a template to display the current state: + +```json +"Debug": "DisplayState", +``` + +Use `$Comment` fields for documentation: + +```json +"$Comment": "Check for any hostile targets in range that could alert the NPC" +``` + +--- + +## NPC Design Process + +1. **Read design requirements** — Understand what the NPC should do. +2. **Decide on states** — Break behavior into top-level states (Idle, Sleep, Eat, Combat, etc.) and substates. +3. **Find reusable components** — Check existing `Component_Instruction_*` and `Component_Sensor_*` files. +4. **Identify reusable parts** — Extract common logic into new components. +5. **Build incrementally** — Add one behavior at a time and **test after each addition**. +6. **Parameterize** — Expose configurable values so variants can customize behavior. + +### Common Template Header Fields + +```json +{ + "Type": "Abstract", + "Debug": "DisplayState", + "StartState": "Idle", + "DefaultPlayerAttitude": "Hostile", + "DefaultNPCAttitude": "Ignore", + "AttitudeGroup": { "Compute": "AttitudeGroup" }, + "KnockbackScale": 0.5, + "Appearance": { "Compute": "Appearance" }, + "DropList": { "Compute": "DropList" }, + "MaxHealth": { "Compute": "MaxHealth" }, + "NameTranslationKey": { "Compute": "NameTranslationKey" } +} +``` + +--- + +## Key Points + +1. **Templates are Abstract, Variants are concrete** — Templates define reusable behavior; variants provide specific values. +2. **States are the backbone** — Every NPC behavior is organized into states and substates. +3. **Test incrementally** — Add one behavior at a time and test before moving on. +4. **Parameterize everything** — Use `Parameters` + `{ "Compute": "..." }` so variants can customize. +5. **Extract components** — If logic appears in multiple places, make it a component. +6. **Detection priority** — Place `Damage_Check` first, then `Standard_Detection`, then state-specific logic. +7. **Use state transitions for visual polish** — Animations, inventory swaps, and beacon messages. +8. **Beacons for inter-NPC communication** — Don't hard-code NPC coupling; use message passing. +9. **Leash prevents runaway NPCs** — Always add `Soft_Leash` in combat to prevent infinite chasing. +10. **`UseTarget: false`** — Required for actions that modify the NPC itself (inventory, stats). + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Template won't compile | Check that all referenced components exist and state names match | +| NPC stuck in one state | Verify state switch actions and `ActionsBlocking` flags | +| NPC doesn't detect player | Check `ViewRange`, `ViewSector`, `HearingRange`, `AbsoluteDetectionRange` parameters | +| NPC chases forever | Add `Component_Instruction_Soft_Leash` with proper `LeashDistance` | +| Animations not playing | Ensure `PlayAnimation` action uses correct `Slot` and animation name | +| NPC ignores damage | Add `Component_Instruction_Damage_Check` to each state's instructions | +| Items not equipping | Call inventory operations with `"UseTarget": false` | +| State transitions not firing | Ensure `StateTransitions` block is above `Instructions` in the JSON | +| Beacon messages not received | Verify NPC groups and beacon `Range` parameter | +| Food NPC not spawning | Check spawn beacon exists, is placed in world, and `TriggerSpawnBeacon` range is sufficient | + +--- + +## Related Skills + +- `hytale-spawning-npcs` — Programmatic NPC spawning via Java (NPCPlugin API) +- `hytale-ecs` — Entity Component System patterns +- `hytale-items` — Item registry, ItemStack, and interactions +- `hytale-entity-effects` — Status effects and buffs +- `hytale-events` — Event system for reacting to NPC-related events + +--- + +## Reference + +- Source: [Hytale Modding - NPC Tutorial](https://hytalemodding.dev/en/docs/official-documentation/npc) diff --git a/skills/hytale-permissions/SKILL.md b/skills/hytale-permissions/SKILL.md new file mode 100644 index 0000000..38b1be4 --- /dev/null +++ b/skills/hytale-permissions/SKILL.md @@ -0,0 +1,495 @@ +--- +name: hytale-permissions +description: Manages permission nodes and groups in Hytale plugins using PermissionsModule. Use when checking player permissions, creating permission groups, adding/removing user permissions, implementing custom permission providers, or listening to permission events. Triggers - permission, PermissionsModule, hasPermission, group, PermissionProvider, wildcard, PermissionHolder, addUserPermission, addGroupPermission, PlayerPermissionChangeEvent, PlayerGroupEvent. +--- + +# Hytale Permission Management + +Use this skill when managing permission nodes and groups in Hytale plugins. Permissions control what actions players can perform on the server, from basic commands to advanced administrative functions. + +--- + +## Quick Reference + +| Operation | Method | +|-----------|--------| +| Check permission | `PermissionsModule.get().hasPermission(uuid, node)` | +| Check with default | `PermissionsModule.get().hasPermission(uuid, node, defaultValue)` | +| Add user permission | `PermissionsModule.get().addUserPermission(uuid, Set.of(...))` | +| Remove user permission | `PermissionsModule.get().removeUserPermission(uuid, Set.of(...))` | +| Add group permission | `PermissionsModule.get().addGroupPermission(groupName, Set.of(...))` | +| Remove group permission | `PermissionsModule.get().removeGroupPermission(groupName, Set.of(...))` | +| Add user to group | `PermissionsModule.get().addUserToGroup(uuid, groupName)` | +| Remove user from group | `PermissionsModule.get().removeUserFromGroup(uuid, groupName)` | +| Get user's groups | `PermissionsModule.get().getGroupsForUser(uuid)` | + +--- + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| Permission Node | A string like `myplugin.feature.use` representing a specific permission | +| Group | A named collection of permissions (e.g., "Admin", "VIP", "Default") | +| Provider | The backend that stores and retrieves permissions | +| Wildcard | A pattern like `*` or `myplugin.*` that matches multiple permissions | + +### Permission Check Order + +1. **User's direct permissions** — Permissions granted directly to the player +2. **Group permissions** — Permissions from groups the player belongs to +3. **Virtual groups** — Game-mode-based permissions (e.g., Creative mode grants builder tools) +4. **Default value** — Falls back to `false` if no match found + +> The first definitive match wins. If a player has a permission granted at the user level, group permissions won't override it. + +--- + +## Accessing PermissionsModule + +```java +PermissionsModule perms = PermissionsModule.get(); +``` + +### Limitations + +The module does **not** support: +- Listing all defined groups +- Deleting a group entirely + +Groups are implicitly created when you first add permissions to them. + +--- + +## Checking Permissions + +### Basic Check + +```java +boolean canUse = PermissionsModule.get().hasPermission(playerUUID, "myplugin.feature.use"); +if (canUse) { + // Player has permission +} else { + // Player lacks permission +} +``` + +### Check with Default Value + +Returns the default when no explicit permission is set — useful for features enabled by default that can be revoked: + +```java +boolean canUse = PermissionsModule.get().hasPermission(playerUUID, "myplugin.feature.use", true); +``` + +### Using PermissionHolder Interface + +Players and command senders implement `PermissionHolder`, allowing direct checks: + +```java +// In a command or event handler where you have access to the player +if (player.hasPermission("myplugin.admin.manage")) { + // Player has admin permission +} +``` + +--- + +## Managing User Permissions + +### Adding Permissions + +Permissions are additive — new permissions are added on top of existing ones, not replaced. + +```java +PermissionsModule perms = PermissionsModule.get(); + +// Add a single permission +perms.addUserPermission(playerUUID, Set.of("myplugin.vip.chat")); + +// Add multiple permissions at once +Set newPerms = Set.of( + "myplugin.vip.chat", + "myplugin.vip.fly", + "myplugin.vip.kit" +); +perms.addUserPermission(playerUUID, newPerms); +``` + +### Removing Permissions + +```java +PermissionsModule perms = PermissionsModule.get(); + +Set toRemove = Set.of("myplugin.vip.fly"); +perms.removeUserPermission(playerUUID, toRemove); +``` + +--- + +## Managing Groups + +Groups let you assign a collection of permissions to multiple players. + +### Adding Permissions to a Group + +If the group doesn't exist, it will be created automatically. + +```java +PermissionsModule perms = PermissionsModule.get(); + +Set vipPerms = Set.of( + "myplugin.vip.chat", + "myplugin.vip.fly", + "myplugin.vip.kit" +); +perms.addGroupPermission("VIP", vipPerms); +``` + +### Removing Permissions from a Group + +```java +PermissionsModule perms = PermissionsModule.get(); + +Set toRemove = Set.of("myplugin.vip.fly"); +perms.removeGroupPermission("VIP", toRemove); +``` + +### Adding a User to a Group + +```java +PermissionsModule perms = PermissionsModule.get(); +perms.addUserToGroup(playerUUID, "VIP"); +``` + +### Removing a User from a Group + +```java +PermissionsModule perms = PermissionsModule.get(); +perms.removeUserFromGroup(playerUUID, "VIP"); +``` + +### Getting a User's Groups + +```java +PermissionsModule perms = PermissionsModule.get(); +Set groups = perms.getGroupsForUser(playerUUID); +for (String group : groups) { + System.out.println("Player is in group: " + group); +} +``` + +--- + +## Built-in Groups + +| Group | Permissions | Description | +|-------|-------------|-------------| +| OP | `*` (all permissions) | Server operators with full access | +| Default | None | Base group for all players | + +Players without any explicit group assignment are automatically part of the `Default` group. + +--- + +## Wildcards + +Wildcards grant or deny multiple permissions with a single pattern. + +| Pattern | Effect | +|---------|--------| +| `*` | Grants all permissions | +| `myplugin.*` | Grants all permissions starting with `myplugin.` | +| `-*` | Denies all permissions | +| `-myplugin.admin.*` | Denies all admin permissions in the plugin | + +### Wildcard Examples + +```java +// Grant all permissions to admins +perms.addGroupPermission("Admin", Set.of("*")); + +// Grant all plugin permissions to moderators +perms.addGroupPermission("Moderator", Set.of("myplugin.*")); + +// Grant all permissions except admin commands +perms.addGroupPermission("Helper", Set.of( + "*", + "-myplugin.admin.*" // Deny admin permissions +)); +``` + +> **Warning:** Negation permissions (starting with `-`) take precedence at each level. Use them carefully to avoid accidentally blocking permissions. + +--- + +## Built-in Permission Nodes + +### HytalePermissions Utility + +```java +// Generate permission for a command +String perm = HytalePermissions.fromCommand("gamemode"); +// Result: "hytale.command.gamemode" + +// Generate permission for a subcommand +String perm = HytalePermissions.fromCommand("gamemode", "creative"); +// Result: "hytale.command.gamemode.creative" +``` + +### Common Permission Nodes + +| Node | Description | +|------|-------------| +| `hytale.command.op.add` | Add players to OP group | +| `hytale.command.op.remove` | Remove players from OP group | +| `hytale.editor.brush.use` | Use brush tools | +| `hytale.editor.prefab.use` | Use prefabs | +| `hytale.editor.selection.use` | Use selection tools | +| `hytale.editor.history` | Undo/redo operations | +| `hytale.camera.flycam` | Use fly camera mode | + +--- + +## Listening to Permission Events + +### Available Events + +| Event | Trigger | +|-------|---------| +| `PlayerPermissionChangeEvent.PermissionsAdded` | Permissions added to a user | +| `PlayerPermissionChangeEvent.PermissionsRemoved` | Permissions removed from a user | +| `PlayerGroupEvent.Added` | User added to a group | +| `PlayerGroupEvent.Removed` | User removed from a group | +| `GroupPermissionChangeEvent.Added` | Permissions added to a group | +| `GroupPermissionChangeEvent.Removed` | Permissions removed from a group | + +### Event Listener Example + +```java +public class PermissionListener { + public static void onPermissionsAdded(PlayerPermissionChangeEvent.PermissionsAdded event) { + UUID playerUUID = event.getPlayerUuid(); + Set added = event.getAddedPermissions(); + System.out.println("Permissions added to " + playerUUID + ": " + added); + } + + public static void onGroupAdded(PlayerGroupEvent.Added event) { + UUID playerUUID = event.getPlayerUuid(); + String groupName = event.getGroupName(); + System.out.println("Player " + playerUUID + " joined group: " + groupName); + } +} +``` + +### Registering Events + +```java +public class MyPlugin extends JavaPlugin { + @Override + public void setup() { + EventRegistry events = this.getEventRegistry(); + events.registerGlobal( + PlayerPermissionChangeEvent.PermissionsAdded.class, + PermissionListener::onPermissionsAdded + ); + events.registerGlobal( + PlayerGroupEvent.Added.class, + PermissionListener::onGroupAdded + ); + } +} +``` + +--- + +## Creating a Custom Permission Provider + +Useful for database-backed permissions, cross-server synchronization, or features like permission expiry. + +### Implementing PermissionProvider + +```java +public class DatabasePermissionProvider implements PermissionProvider { + @Nonnull + @Override + public String getName() { + return "DatabasePermissionProvider"; + } + + // User permission methods + @Override + public void addUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + // Save to your database + } + + @Override + public void removeUserPermissions(@Nonnull UUID uuid, @Nonnull Set permissions) { + // Remove from your database + } + + @Override + public Set getUserPermissions(@Nonnull UUID uuid) { + // Query your database + return Set.of(); + } + + // Group permission methods + @Override + public void addGroupPermissions(@Nonnull String group, @Nonnull Set permissions) { + // Save to your database + } + + @Override + public void removeGroupPermissions(@Nonnull String group, @Nonnull Set permissions) { + // Remove from your database + } + + @Override + public Set getGroupPermissions(@Nonnull String group) { + // Query your database + return Set.of(); + } + + // User-group membership methods + @Override + public void addUserToGroup(@Nonnull UUID uuid, @Nonnull String group) { + // Save to your database + } + + @Override + public void removeUserFromGroup(@Nonnull UUID uuid, @Nonnull String group) { + // Remove from your database + } + + @Override + public Set getGroupsForUser(@Nonnull UUID uuid) { + // Query your database + return Set.of("Default"); + } +} +``` + +> **Warning:** Hytale automatically assigns players to game-mode groups (like `Creative` or `Adventure`) using the first provider. If your provider throws an error when the group doesn't exist, the player will be disconnected! Always handle missing groups gracefully. + +### Registering Your Provider + +```java +public class MyPlugin extends JavaPlugin { + @Override + public void setup() { + // Add your provider alongside the default one + PermissionsModule.get().addProvider(new DatabasePermissionProvider()); + } +} +``` + +The `PermissionsModule` aggregates permissions from all registered providers: +- Permission checks query **all** providers +- Write operations (add/remove) use the **first** provider + +### Replacing the Default Provider + +If you want full control, remove the default provider first: + +```java +public class MyPlugin extends JavaPlugin { + @Override + public void setup() { + PermissionsModule perms = PermissionsModule.get(); + + // Remove the default provider FIRST + perms.removeProvider(perms.getFirstPermissionProvider()); + + // Then add your provider + perms.addProvider(new DatabasePermissionProvider()); + } +} +``` + +> **Note:** When you remove the default provider, the `/op self` command will stop working since it checks for provider tampering. + +--- + +## Best Practices + +### Permission Naming Conventions + +Follow the pattern: `namespace.category.action` + +```java +// Good - clear hierarchy +"myplugin.admin.ban" +"myplugin.user.teleport.home" +"myplugin.vip.chat.color" + +// Bad - unclear structure +"myplugin_ban" +"teleport" +"vipChatColor" +``` + +### Use Constants for Permissions + +Define permissions as constants to avoid typos: + +```java +public final class MyPermissions { + public static final String ADMIN_BAN = "myplugin.admin.ban"; + public static final String ADMIN_KICK = "myplugin.admin.kick"; + public static final String USER_HOME = "myplugin.user.home"; + public static final String VIP_FLY = "myplugin.vip.fly"; +} + +// Usage +if (player.hasPermission(MyPermissions.ADMIN_BAN)) { + // ... +} +``` + +### Don't Over-Permission + +Only create permissions for actions that genuinely need access control. Not everything needs a permission check. + +### Thread Safety + +If you create a custom provider, ensure it's thread-safe. Permission checks can occur from multiple threads simultaneously. Use `ReadWriteLock` or `ConcurrentHashMap` for your data structures. + +--- + +## In-Game Commands + +### /op Command + +| Command | Description | +|---------|-------------| +| `/op self` | Toggle your own OP status (singleplayer or with `--allow-op` flag) | +| `/op add ` | Add a player to the OP group | +| `/op remove ` | Remove a player from the OP group | + +### /perm Command + +| Command | Description | +|---------|-------------| +| `/perm user list ` | List user's permissions | +| `/perm user add ` | Add permission to user | +| `/perm user remove ` | Remove permission from user | +| `/perm user group list ` | List user's groups | +| `/perm user group add ` | Add user to group | +| `/perm user group remove ` | Remove user from group | +| `/perm group list ` | List group's permissions | +| `/perm group add ` | Add permission to group | +| `/perm group remove ` | Remove permission from group | +| `/perm test ` | Test if you have a permission | + +--- + +## Related Classes + +- `com.hypixel.hytale.server.core.permissions.PermissionsModule` +- `com.hypixel.hytale.server.core.permissions.PermissionProvider` +- `com.hypixel.hytale.server.core.permissions.PermissionHolder` +- `com.hypixel.hytale.server.core.permissions.HytalePermissions` +- `com.hypixel.hytale.server.core.permissions.PlayerPermissionChangeEvent` +- `com.hypixel.hytale.server.core.permissions.PlayerGroupEvent` +- `com.hypixel.hytale.server.core.permissions.GroupPermissionChangeEvent` diff --git a/skills/hytale-persistent-data/SKILL.md b/skills/hytale-persistent-data/SKILL.md new file mode 100644 index 0000000..a26212c --- /dev/null +++ b/skills/hytale-persistent-data/SKILL.md @@ -0,0 +1,438 @@ +--- +name: hytale-persistent-data +description: Stores persistent data on players and entities using custom components with Codec serialization in Hytale plugins. Use when saving player data across sessions, creating custom player components, serializing complex data types to BSON, or persisting entity state. Triggers - persistent data, player data, save data, BuilderCodec, KeyedCodec, putComponent, ensureAndGetComponent, BSON serialization, player state, custom component, session data. +--- + +# Hytale Persistent Data Storage + +This skill provides comprehensive documentation for storing persistent data on players and entities using custom components with Codec serialization. + +## Quick Reference + +| Task | Approach | +|------|----------| +| Create persistent component | Class implementing `Component` with `BuilderCodec` | +| Register component | `getEntityStoreRegistry().registerComponent(Class, name, CODEC)` in `setup()` | +| Add temporary component | `store.addComponent(ref, componentType, instance)` | +| Add persistent component | `store.putComponent(ref, componentType, instance)` | +| Get or create component | `store.ensureAndGetComponent(ref, componentType)` | +| Check if exists | `store.getComponent(ref, componentType) != null` | +| Serialize primitives | `Codec.INTEGER`, `Codec.STRING`, `Codec.BOOLEAN`, `Codec.FLOAT`, `Codec.DOUBLE` | +| Serialize collections | `MapCodec`, `ListCodec`, `SetCodec` | + +--- + +## Component Class Structure + +### Required Elements + +Every persistent component must have: + +1. **Fields** - Data to persist +2. **BuilderCodec** - Serialization definition with getters/setters for each field +3. **Default constructor** - Initializes default values +4. **Copy constructor** - For cloning +5. **clone() method** - Returns new instance via copy constructor + +### Basic Template + +```java +import com.hypixel.hytale.codec.BuilderCodec; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.ecs.Component; +import com.hypixel.hytale.ecs.entity.store.EntityStore; + +import javax.annotation.Nonnull; + +public class CustomPlayerData implements Component { + + // === Fields === + private int someInteger; + private String someString; + + // === Codec Definition === + public static final BuilderCodec CODEC = + BuilderCodec.builder(CustomPlayerData.class, CustomPlayerData::new) + .append(new KeyedCodec<>("SomeInteger", Codec.INTEGER), + (data, value) -> data.someInteger = value, // setter + data -> data.someInteger) // getter + .addValidator(Validators.nonNull()) + .add() + .append(new KeyedCodec<>("SomeString", Codec.STRING), + (data, value) -> data.someString = value, + data -> data.someString) + .add() + .build(); + + // === Default Constructor === + public CustomPlayerData() { + this.someInteger = 0; + this.someString = ""; + } + + // === Copy Constructor === + public CustomPlayerData(CustomPlayerData clone) { + this.someInteger = clone.someInteger; + this.someString = clone.someString; + } + + // === Clone Method === + @Nonnull + @Override + public Component clone() { + return new CustomPlayerData(this); + } + + // === Getters and Setters === + public int getSomeInteger() { return someInteger; } + public void setSomeInteger(int value) { this.someInteger = value; } + + public String getSomeString() { return someString; } + public void setSomeString(String value) { this.someString = value; } +} +``` + +--- + +## Codec System + +### KeyedCodec Requirements + +> **IMPORTANT**: The key in `KeyedCodec` must start with a Capital Letter, otherwise serialization may fail. + +```java +// ✅ Correct - Capital first letter +new KeyedCodec<>("SomeInteger", Codec.INTEGER) + +// ❌ Wrong - lowercase first letter +new KeyedCodec<>("someInteger", Codec.INTEGER) +``` + +### Primitive Codecs + +| Type | Codec | +|------|-------| +| `int` | `Codec.INTEGER` | +| `long` | `Codec.LONG` | +| `float` | `Codec.FLOAT` | +| `double` | `Codec.DOUBLE` | +| `boolean` | `Codec.BOOLEAN` | +| `String` | `Codec.STRING` | + +### Collection Codecs + +```java +// Map +new KeyedCodec<>("SomeMap", + new MapCodec<>(Codec.STRING, HashMap::new, false)) + +// List +new KeyedCodec<>("SomeList", + new ListCodec<>(Codec.STRING, ArrayList::new)) + +// Set +new KeyedCodec<>("SomeSet", + new SetCodec<>(Codec.INTEGER, HashSet::new)) +``` + +### BuilderCodec Chain Pattern + +```java +public static final BuilderCodec CODEC = + BuilderCodec.builder(MyComponent.class, MyComponent::new) + // Field 1 + .append(new KeyedCodec<>("FieldOne", Codec.INTEGER), + (data, value) -> data.fieldOne = value, + data -> data.fieldOne) + .addValidator(Validators.nonNull()) // Optional validator + .add() + // Field 2 + .append(new KeyedCodec<>("FieldTwo", Codec.STRING), + (data, value) -> data.fieldTwo = value, + data -> data.fieldTwo) + .add() + // Field 3 with collection + .append(new KeyedCodec<>("FieldThree", + new MapCodec<>(Codec.STRING, HashMap::new, false)), + (data, value) -> data.fieldThree = value, + data -> data.fieldThree) + .add() + .build(); +``` + +--- + +## Component Registration + +Register the component in your plugin's `setup()` method: + +```java +public class MyPlugin extends JavaPlugin { + + private ComponentType customPlayerDataComponent; + + public MyPlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + // Register the component with its codec + this.customPlayerDataComponent = this.getEntityStoreRegistry().registerComponent( + CustomPlayerData.class, + "CustomPlayerDataComponent", + CustomPlayerData.CODEC + ); + } + + // Getter for other classes to access + public ComponentType getCustomPlayerDataComponent() { + return this.customPlayerDataComponent; + } +} +``` + +--- + +## Using Components + +### addComponent vs putComponent + +| Method | Persistence | Use Case | +|--------|-------------|----------| +| `addComponent` | Temporary | Component removed when entity leaves world | +| `putComponent` | Persistent | Component saved and loaded across sessions | + +### Adding/Updating Data + +```java +private void updatePlayerData( + @Nonnull Ref ref, + @Nonnull Store store +) { + ComponentType componentType = + MyPlugin.instance().getCustomPlayerDataComponent(); + + // Check if component already exists + CustomPlayerData existing = store.getComponent(ref, componentType); + + if (existing != null) { + // Update existing component + existing.setSomeString("Updated Value"); + existing.setSomeInteger(existing.getSomeInteger() + 1); + } else { + // Create and put new component + CustomPlayerData newData = new CustomPlayerData(); + newData.setSomeString("Initial Value"); + newData.setSomeInteger(1); + + // Use putComponent for persistence + store.putComponent(ref, componentType, newData); + } +} +``` + +### Retrieving Data (with auto-creation) + +Use `ensureAndGetComponent` to get the component, creating it with default values if it doesn't exist: + +```java +public class MyCommand extends AbstractPlayerCommand { + + public MyCommand() { + super("mycommand", "Description here"); + } + + @Override + protected void execute( + @Nonnull CommandContext commandContext, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world + ) { + ComponentType componentType = + MyPlugin.instance().getCustomPlayerDataComponent(); + + // Gets component or creates with default values + CustomPlayerData data = store.ensureAndGetComponent(ref, componentType); + + // Use the data + int currentValue = data.getSomeInteger(); + String currentString = data.getSomeString(); + + // Modify if needed + data.setSomeInteger(currentValue + 1); + } +} +``` + +--- + +## Complete Example + +### Component Class + +```java +package com.example.plugin.components; + +import com.hypixel.hytale.codec.BuilderCodec; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.MapCodec; +import com.hypixel.hytale.codec.Validators; +import com.hypixel.hytale.ecs.Component; +import com.hypixel.hytale.ecs.entity.store.EntityStore; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public class PlayerStats implements Component { + + private int kills; + private int deaths; + private long playTime; + private Map achievements; + + public static final BuilderCodec CODEC = + BuilderCodec.builder(PlayerStats.class, PlayerStats::new) + .append(new KeyedCodec<>("Kills", Codec.INTEGER), + (data, value) -> data.kills = value, + data -> data.kills) + .addValidator(Validators.nonNull()) + .add() + .append(new KeyedCodec<>("Deaths", Codec.INTEGER), + (data, value) -> data.deaths = value, + data -> data.deaths) + .addValidator(Validators.nonNull()) + .add() + .append(new KeyedCodec<>("PlayTime", Codec.LONG), + (data, value) -> data.playTime = value, + data -> data.playTime) + .add() + .append(new KeyedCodec<>("Achievements", + new MapCodec<>(Codec.INTEGER, HashMap::new, false)), + (data, value) -> data.achievements = value, + data -> data.achievements) + .add() + .build(); + + public PlayerStats() { + this.kills = 0; + this.deaths = 0; + this.playTime = 0L; + this.achievements = new HashMap<>(); + } + + public PlayerStats(PlayerStats clone) { + this.kills = clone.kills; + this.deaths = clone.deaths; + this.playTime = clone.playTime; + this.achievements = new HashMap<>(clone.achievements); + } + + @Nonnull + @Override + public Component clone() { + return new PlayerStats(this); + } + + // Getters and setters + public int getKills() { return kills; } + public void setKills(int kills) { this.kills = kills; } + public void incrementKills() { this.kills++; } + + public int getDeaths() { return deaths; } + public void setDeaths(int deaths) { this.deaths = deaths; } + public void incrementDeaths() { this.deaths++; } + + public long getPlayTime() { return playTime; } + public void setPlayTime(long playTime) { this.playTime = playTime; } + public void addPlayTime(long time) { this.playTime += time; } + + public Map getAchievements() { return achievements; } + public void unlockAchievement(String id) { + achievements.put(id, achievements.getOrDefault(id, 0) + 1); + } +} +``` + +### Plugin Registration + +```java +package com.example.plugin; + +import com.example.plugin.components.PlayerStats; +import com.hypixel.hytale.ecs.entity.store.EntityStore; +import com.hypixel.hytale.ecs.query.ComponentType; +import com.hypixel.hytale.plugin.JavaPlugin; +import com.hypixel.hytale.plugin.JavaPluginInit; + +import javax.annotation.Nonnull; + +public class MyPlugin extends JavaPlugin { + + private static MyPlugin instance; + private ComponentType playerStatsComponent; + + public MyPlugin(@Nonnull JavaPluginInit init) { + super(init); + instance = this; + } + + @Override + protected void setup() { + this.playerStatsComponent = this.getEntityStoreRegistry().registerComponent( + PlayerStats.class, + "PlayerStatsComponent", + PlayerStats.CODEC + ); + } + + public static MyPlugin instance() { return instance; } + + public ComponentType getPlayerStatsComponent() { + return this.playerStatsComponent; + } +} +``` + +--- + +## Best Practices + +### Naming Conventions + +| Element | Convention | Example | +|---------|------------|---------| +| Component class | PascalCase, descriptive | `PlayerStats`, `QuestProgress` | +| Component name (registration) | PascalCase + "Component" | `"PlayerStatsComponent"` | +| KeyedCodec keys | PascalCase, starts with capital | `"Kills"`, `"PlayTime"` | +| Fields | camelCase | `kills`, `playTime` | + +### Performance Tips + +1. **Avoid frequent getComponent calls** - Cache the component reference when processing multiple operations +2. **Use ensureAndGetComponent wisely** - It creates a new component if none exists, which may not always be desired +3. **Batch updates** - Modify multiple fields before the component is saved +4. **Keep components focused** - One component per logical data grouping + +### Common Pitfalls + +| Issue | Solution | +|-------|----------| +| Data not persisting | Use `putComponent` instead of `addComponent` | +| Serialization fails | Ensure KeyedCodec keys start with capital letter | +| NullPointerException | Initialize collections in default constructor | +| Clone issues | Deep copy collections in copy constructor | + +--- + +## Related Resources + +- [ECS Theory Guide](https://hytalemodding.dev/en/docs/guides/ecs/hytale-ecs-theory) +- [Entity Component System](https://hytalemodding.dev/en/docs/guides/ecs/entity-component-system) +- [Systems Guide](https://hytalemodding.dev/en/docs/guides/ecs/systems) +- Codec types: `lib/hytale-server/src/main/java/com/hypixel/hytale/codec/` diff --git a/skills/hytale-player-death-event/SKILL.md b/skills/hytale-player-death-event/SKILL.md new file mode 100644 index 0000000..ce7fefa --- /dev/null +++ b/skills/hytale-player-death-event/SKILL.md @@ -0,0 +1,198 @@ +--- +name: hytale-player-death-event +description: Documents how to detect and react to player death in Hytale plugins using DeathSystems.OnDeathSystem. Use when handling player death, reading death cause/damage, broadcasting death messages, or building respawn logic. Triggers - death, player death, OnDeathSystem, DeathSystems, DeathComponent, death event, death handler, death info, death damage, death cause, respawn, kill feed, death message. +--- + +# Hytale Player Death Event + +Use this skill when reacting to player (or entity) death in a Hytale plugin. Death detection uses the ECS `RefChangeSystem` pattern — specifically `DeathSystems.OnDeathSystem` — which fires when a `DeathComponent` is added to an entity. + +> **Source:** +> +> **Related skills:** `hytale-events` (general event system), `hytale-ecs` (ECS fundamentals), `hytale-entity-effects` (effects/damage pipeline). + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Detect player death | Extend `DeathSystems.OnDeathSystem`, query for `Player` | +| Get the player who died | `store.getComponent(ref, Player.getComponentType())` | +| Get death damage info | `component.getDeathInfo()` → `Damage` | +| Get damage amount | `deathInfo.getAmount()` | +| Broadcast a death message | `Universe.get().sendMessage(Message.raw(...))` | +| Register the system | `getEntityStoreRegistry().registerSystem(...)` in `start()` | + +--- + +## How It Works + +1. When an entity's health reaches zero, the damage pipeline attaches a `DeathComponent` to the entity. +2. Any system extending `DeathSystems.OnDeathSystem` whose `getQuery()` matches the entity is notified via `onComponentAdded`. +3. The `DeathComponent` carries a `Damage` object with information about the killing blow (amount, source, etc.). + +This is a `RefChangeSystem` — it reacts to component lifecycle changes, not tick-based updates. + +--- + +## Required Imports + +```java +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.server.core.Message; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.modules.entity.damage.Damage; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathSystems; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import javax.annotation.Nonnull; +``` + +--- + +## Step-by-Step + +### 1. Create a class extending `DeathSystems.OnDeathSystem` + +```java +public class PlayerDeathHandler extends DeathSystems.OnDeathSystem { +``` + +`OnDeathSystem` is a specialised `RefChangeSystem` that listens for `DeathComponent` additions. + +### 2. Define which entities to watch with `getQuery()` + +```java +@Nonnull +@Override +public Query getQuery() { + return Query.and(Player.getComponentType()); +} +``` + +`Query.and(Player.getComponentType())` ensures only entities that have a `Player` component trigger this system. You can combine multiple component types to narrow the filter further. + +### 3. Override `onComponentAdded` to handle the death + +```java +@Override +public void onComponentAdded( + @Nonnull Ref ref, + @Nonnull DeathComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + + Player playerComponent = (Player) store.getComponent(ref, Player.getComponentType()); + assert playerComponent != null; + + // React to the death + Universe.get().sendMessage( + Message.raw("Player died: " + playerComponent.getDisplayName())); + + // Access death damage info + Damage deathInfo = component.getDeathInfo(); + if (deathInfo != null) { + Universe.get().sendMessage( + Message.raw("Damage amount: " + deathInfo.getAmount())); + } +} +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ref` | `Ref` | Handle to the entity that died | +| `component` | `DeathComponent` | Contains death cause info via `getDeathInfo()` | +| `store` | `Store` | ECS store for looking up other components on the entity | +| `commandBuffer` | `CommandBuffer` | Buffer for queuing entity/component mutations | + +### 4. Register the system in your plugin's `start()` method + +```java +@Override +protected void start() { + this.getEntityStoreRegistry().registerSystem(new PlayerDeathHandler()); +} +``` + +Death systems are registered the same way as any other ECS system — via `getEntityStoreRegistry().registerSystem()` in the plugin's `start()` lifecycle method. + +--- + +## Complete Example + +```java +package com.example.plugin.systems; + +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.server.core.Message; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.modules.entity.damage.Damage; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathSystems; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import javax.annotation.Nonnull; + +public class PlayerDeathHandler extends DeathSystems.OnDeathSystem { + + @Nonnull + @Override + public Query getQuery() { + return Query.and(Player.getComponentType()); + } + + @Override + public void onComponentAdded( + @Nonnull Ref ref, + @Nonnull DeathComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + + Player playerComponent = (Player) store.getComponent(ref, Player.getComponentType()); + assert playerComponent != null; + + Universe.get().sendMessage( + Message.raw("Player died: " + playerComponent.getDisplayName())); + + Damage deathInfo = component.getDeathInfo(); + if (deathInfo != null) { + Universe.get().sendMessage( + Message.raw("Damage amount: " + deathInfo.getAmount())); + } + } +} +``` + +--- + +## Tips + +- **Null-check `getDeathInfo()`** — it can be `null` if the entity was removed without going through the damage pipeline (e.g., `/kill` or direct removal). +- **Use `Query.and(...)` with multiple component types** to restrict death handling to specific entity subsets (e.g., players with a certain custom component). +- **Exclude dead entities from other systems** by adding `Query.not(DeathComponent.getComponentType())` to their queries so they stop processing dead entities immediately. +- **Use `CommandBuffer`** (not direct store mutation) if you need to add/remove components in response to a death — this ensures thread safety and proper ordering. +- **Localize death messages** using `Message.translation(...)` instead of `Message.raw(...)` for user-facing text. + +--- + +## Key Classes + +| Class | Package | Purpose | +|-------|---------|---------| +| `DeathSystems.OnDeathSystem` | `...modules.entity.damage` | Base class for death-reaction systems | +| `DeathComponent` | `...modules.entity.damage` | Component added on death; carries `Damage` info | +| `Damage` | `...modules.entity.damage` | Death cause data (amount, source) | +| `Player` | `...core.entity.entities` | Player component for identity/display name | +| `Query` | `...component.query` | Entity filter for system targeting | +| `Store` | `...component` | ECS data store for component lookups | +| `CommandBuffer` | `...component` | Thread-safe mutation buffer | diff --git a/skills/hytale-player-input/SKILL.md b/skills/hytale-player-input/SKILL.md new file mode 100644 index 0000000..ba79553 --- /dev/null +++ b/skills/hytale-player-input/SKILL.md @@ -0,0 +1,629 @@ +--- +name: hytale-player-input +description: Documents Hytale's player input system including packet interception (PacketAdapters, PacketWatcher, PacketFilter), SyncInteractionChains, InteractionTypes, client-to-server packet reference, and custom camera controls. Use when handling player input, intercepting packets, creating custom interactions, modifying camera behavior, or working with mouse/keyboard input. Triggers - player input, packet, PacketAdapters, PacketWatcher, PacketFilter, PlayerPacketWatcher, PlayerPacketFilter, SyncInteractionChains, InteractionType, MouseInteraction, ClientMovement, camera, SetServerCamera, ServerCameraSettings, camera controls, top-down, isometric, side-scroller, inbound packet, outbound packet, packet listener, input handling. +--- + +# Hytale Player Input Skill + +Use this skill when working with player input handling in Hytale plugins. This covers how the client communicates input to the server via packets, how to intercept and filter those packets, all InteractionTypes, the complete client-to-server packet reference, and custom camera controls. + +> **Related skills:** For hotbar-specific slot customization (ability slots), see `hytale-hotbar-actions`. For game events (PlayerReady, chat, damage, etc.), see `hytale-events`. For UI-based input, see `hytale-ui-modding`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Listen to all inbound packets | `PacketAdapters.registerInbound((PacketWatcher) ...)` | +| Listen to player-specific inbound packets | `PacketAdapters.registerInbound((PlayerPacketWatcher) ...)` | +| Block/cancel inbound packets | `PacketAdapters.registerInbound((PlayerPacketFilter) ...)` — return `true` to cancel | +| Listen to outbound packets | `PacketAdapters.registerOutbound((PacketWatcher) ...)` | +| Detect player interactions (left/right click, F key) | Intercept `SyncInteractionChains` (packet ID 290) | +| Detect mouse input | Intercept `MouseInteraction` (packet ID 111) | +| Detect player movement | Intercept `ClientMovement` (packet ID 108) | +| Customize camera | Send `SetServerCamera` packet with `ServerCameraSettings` | +| Reset camera to default | Send `SetServerCamera(ClientCameraView.Custom, false, null)` | +| Deregister a listener | `PacketAdapters.deregisterInbound(filter)` or `deregisterOutbound(watcher)` | + +--- + +## Part 1: How Player Input Works + +Hytale servers **do not receive raw keyboard input**. The client interprets keypresses and sends **packets** describing what action the player wants to perform. To create custom input behavior, you intercept these packets server-side. + +Key concepts: +- **Inbound packets** = Client → Server (player actions) +- **Outbound packets** = Server → Client (state updates, camera, etc.) +- Packets are defined in `com.hypixel.hytale.protocol` and organized by category in `com.hypixel.hytale.protocol.packets` +- Base class is `Packet`; the low-level Netty handler is `PlayerChannelHandler` which delegates to `PacketAdapters` + +--- + +## Part 2: PacketAdapters System + +The `PacketAdapters` class provides the injection point for packet interception. You do **not** need to hook into Netty manually. + +### Registration Methods + +| Method | Interface Type | Can Block | Player-Specific | +|--------|---------------|-----------|-----------------| +| `registerInbound(PacketWatcher)` | `PacketWatcher` | No | No | +| `registerInbound(PacketFilter)` | `PacketFilter` | Yes | No | +| `registerInbound(PlayerPacketWatcher)` | `PlayerPacketWatcher` | No | Yes | +| `registerInbound(PlayerPacketFilter)` | `PlayerPacketFilter` | Yes | Yes | +| `registerOutbound(PacketWatcher)` | `PacketWatcher` | No | No | +| `registerOutbound(PacketFilter)` | `PacketFilter` | Yes | No | + +### Interfaces + +```java +// Read-only observer — cannot block packets +public interface PacketWatcher { + void accept(PacketHandler packetHandler, Packet packet); +} + +// Can block packets — return true to cancel, false to allow +public interface PacketFilter { + boolean test(PacketHandler packetHandler, Packet packet); +} + +// Player-specific read-only observer +public interface PlayerPacketWatcher { + void accept(@Nonnull PlayerRef playerRef, @Nonnull Packet packet); +} + +// Player-specific filter — return true to cancel, false to allow +public interface PlayerPacketFilter { + boolean test(@Nonnull PlayerRef playerRef, @Nonnull Packet packet); +} +``` + +### Imports + +```java +import com.hypixel.hytale.protocol.Packet; +import com.hypixel.hytale.server.core.io.adapter.PacketAdapters; +import com.hypixel.hytale.server.core.io.adapter.PacketFilter; +import com.hypixel.hytale.server.core.io.adapter.PacketWatcher; +import com.hypixel.hytale.server.core.io.adapter.PlayerPacketFilter; +import com.hypixel.hytale.server.core.io.adapter.PlayerPacketWatcher; +import com.hypixel.hytale.server.core.io.adapter.PacketHandler; +import com.hypixel.hytale.server.core.io.adapter.GamePacketHandler; +import com.hypixel.hytale.server.core.universe.PlayerRef; +``` + +--- + +## Part 3: Intercepting Interactions (SyncInteractionChains) + +When a player performs interactions (left click, right click, F key, etc.), the client sends a `SyncInteractionChains` packet (ID 290) containing `SyncInteractionChain` objects. + +### SyncInteractionChain Fields + +| Field | Description | +|-------|-------------| +| `interactionType` | The `InteractionType` enum value | +| `activeHotbarSlot` | The slot the player is currently on | +| `data.targetSlot` | The slot the player wants to switch to (for swap types) | +| `initial` | Whether this is the start of a new interaction chain | + +### Example: Listening for Use Interaction (F Key) + +```java +public class PacketListener implements PacketWatcher { + @Override + public void accept(PacketHandler packetHandler, Packet packet) { + if (packet.getId() != 290) { + return; + } + SyncInteractionChains interactionChains = (SyncInteractionChains) packet; + SyncInteractionChain[] updates = interactionChains.updates; + + for (SyncInteractionChain item : updates) { + PlayerAuthentication playerAuthentication = packetHandler.getAuth(); + String uuid = playerAuthentication.getUuid().toString(); + InteractionType interactionType = item.interactionType; + if (interactionType == InteractionType.Use) { + // Handle "F" key interaction + } + } + } +} +``` + +### Example: Filtering Interactions (Cancel Specific Actions) + +```java +public class InteractionFilter implements PlayerPacketFilter { + @Override + public boolean test(@Nonnull PlayerRef playerRef, @Nonnull Packet packet) { + if (!(packet instanceof SyncInteractionChains syncPacket)) { + return false; + } + + for (SyncInteractionChain chain : syncPacket.updates) { + if (chain.interactionType == InteractionType.Primary) { + // Block left-click interactions + return true; + } + } + + return false; // Allow all other packets + } +} +``` + +### Interaction Imports + +```java +import com.hypixel.hytale.protocol.InteractionType; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChains; +import com.hypixel.hytale.server.core.auth.PlayerAuthentication; +``` + +--- + +## Part 4: InteractionType Reference + +All interaction types from `InteractionType` enum: + +| Name | Ordinal | Description | +|------|---------|-------------| +| `Primary` | 0 | Left click | +| `Secondary` | 1 | Right click | +| `Ability1` | 2 | Ability slot 1 | +| `Ability2` | 3 | Ability slot 2 | +| `Ability3` | 4 | Ability slot 3 | +| `Use` | 5 | Use key (F) | +| `Pick` | 6 | Pick action | +| `Pickup` | 7 | Pickup action | +| `CollisionEnter` | 8 | Entity collision start | +| `CollisionLeave` | 9 | Entity collision end | +| `Collision` | 10 | Ongoing collision | +| `EntityStatEffect` | 11 | Stat effect applied | +| `SwapTo` | 12 | Switching to a slot | +| `SwapFrom` | 13 | Switching from a slot | +| `Death` | 14 | Entity death | +| `Wielding` | 15 | Wielding an item | +| `ProjectileSpawn` | 16 | Projectile created | +| `ProjectileHit` | 17 | Projectile hits target | +| `ProjectileMiss` | 18 | Projectile misses | +| `ProjectileBounce` | 19 | Projectile bounces | +| `Held` | 20 | Item held in main hand | +| `HeldOffhand` | 21 | Item held in offhand | +| `Equipped` | 22 | Item equipped | +| `Dodge` | 23 | Dodge action | +| `GameModeSwap` | 24 | Game mode changed | + +> **Common input triggers:** `Primary` (left click), `Secondary` (right click), `Use` (F key). For hotbar slot-based ability triggers, see the `hytale-hotbar-actions` skill. + +--- + +## Part 5: Modifying & Observing Packets + +### Observing Outbound Packets (Server → Client) + +```java +PacketAdapters.registerOutbound((PacketHandler handler, Packet packet) -> { + var handlerName = handler.getClass().getSimpleName(); + var packetName = packet.getClass().getSimpleName(); + // Exclude noisy packets + if (!"EntityUpdates".equals(packetName) && !"CachedPacket".equals(packetName)) { + logger.at(Level.INFO) + .log("[" + handlerName + "] Sent packet id=" + packet.getId() + ": " + packetName); + } +}); +``` + +### Modifying Inbound Packets + +```java +PacketAdapters.registerInbound((PacketHandler handler, Packet packet) -> { + if (packet instanceof PlayerOptions skinPacket) { + skinPacket.skin = null; // Remove skin data + } +}); +``` + +### Blocking Player Packets (PlayerPacketFilter) + +```java +PacketAdapters.registerInbound((PlayerPacketFilter) (player, packet) -> { + if (packet instanceof ClientMovement movementPacket) { + // Block movement — return true to cancel + return true; + } + return false; +}); +``` + +> **Warning:** While you can cancel packets, client-side prediction still occurs. The player's client will still show movement locally. Preventing specific player actions requires additional work beyond just cancelling packets. + +### Packet Tracker Utility + +Track all packets sent to/from players for debugging: + +```java +public class PlayerPacketTracker { + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + private static class PlayerStats { + final Map sent = new ConcurrentHashMap<>(); + final Map received = new ConcurrentHashMap<>(); + } + + private static final Map stats = new ConcurrentHashMap<>(); + + private static String getPlayerName(PacketHandler handler) { + if (handler instanceof GamePacketHandler gpHandler) { + return gpHandler.getPlayerRef().getUsername(); + } + return null; + } + + public static void registerPacketCounters() { + PacketAdapters.registerInbound((PacketHandler handler, Packet packet) -> { + String playerName = getPlayerName(handler); + if (playerName != null) { + stats.computeIfAbsent(playerName, k -> new PlayerStats()) + .received.computeIfAbsent(packet.getClass().getSimpleName(), + k -> new AtomicInteger(0)) + .incrementAndGet(); + } + }); + + PacketAdapters.registerOutbound((PacketHandler handler, Packet packet) -> { + String playerName = getPlayerName(handler); + if (playerName != null) { + stats.computeIfAbsent(playerName, k -> new PlayerStats()) + .sent.computeIfAbsent(packet.getClass().getSimpleName(), + k -> new AtomicInteger(0)) + .incrementAndGet(); + } + }); + + // Log every 3 seconds + HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> { + if (stats.isEmpty()) return; + for (Map.Entry entry : stats.entrySet()) { + String player = entry.getKey(); + PlayerStats pStats = entry.getValue(); + StringBuilder sb = new StringBuilder(); + + List sentLogs = new ArrayList<>(); + pStats.sent.forEach((type, atomic) -> { + int count = atomic.getAndSet(0); + if (count > 0) sentLogs.add(type + " x" + count); + }); + if (!sentLogs.isEmpty()) { + sb.append("Sent ").append(String.join(", ", sentLogs)); + } + + List recvLogs = new ArrayList<>(); + pStats.received.forEach((type, atomic) -> { + int count = atomic.getAndSet(0); + if (count > 0) recvLogs.add(type + " x" + count); + }); + if (!recvLogs.isEmpty()) { + if (!sb.isEmpty()) sb.append("\n"); + sb.append("Received ").append(String.join(", ", recvLogs)); + } + + if (!sb.isEmpty()) { + LOGGER.atInfo().log("To " + player + ":\n" + sb); + } + } + }, 3, 3, TimeUnit.SECONDS); + } +} +``` + +Call `PlayerPacketTracker.registerPacketCounters()` in your plugin's `setup()` method. + +--- + +## Part 6: Plugin Registration & Cleanup + +Always store references to registered filters/watchers and deregister them on shutdown: + +```java +public class MyPlugin extends HytaleServerPlugin { + private PacketFilter inboundFilter; + + @Override + protected void setup() { + inboundFilter = PacketAdapters.registerInbound( + (PlayerPacketFilter) (player, packet) -> { + // Your filter logic + return false; + } + ); + } + + @Override + protected void shutdown() { + if (inboundFilter != null) { + PacketAdapters.deregisterInbound(inboundFilter); + } + } +} +``` + +--- + +## Part 7: Client-to-Server Packet Reference + +Packets are found in: `com.hypixel.hytale.protocol.packets` + +### Player Packets + +| Packet | ID | Key Fields | +|--------|----|------------| +| `SetClientId` | 100 | `clientId` | +| `SetGameMode` | 101 | `gameMode` | +| `SetMovementStates` | 102 | `movementStates` | +| `SetBlockPlacementOverride` | 103 | `enabled` | +| `JoinWorld` | 104 | `clearWorld`, `fadeInOut`, `worldUuid` | +| `ClientReady` | 105 | `readyForChunks`, `readyForGameplay` | +| `LoadHotbar` | 106 | `inventoryRow` | +| `SaveHotbar` | 107 | `inventoryRow` | +| `ClientMovement` | 108 | `movementStates`, `relativePosition`, `absolutePosition`, `bodyOrientation`, `lookOrientation`, `teleportAck`, `wishMovement`, `velocity`, `mountedTo`, `riderMovementStates` | +| `ClientTeleport` | 109 | `teleportId`, `modelTransform`, `resetVelocity` | +| `UpdateMovementSettings` | 110 | `movementSettings` | +| `MouseInteraction` | 111 | `clientTimestamp`, `activeSlot`, `itemInHandId`, `screenPoint`, `mouseButton`, `mouseMotion`, `worldInteraction` | +| `DamageInfo` | 112 | `damageSourcePosition`, `damageAmount`, `damageCause` | +| `ReticleEvent` | 113 | `eventIndex` | +| `DisplayDebug` | 114 | `shape`, `matrix`, `color`, `time`, `fade`, `frustumProjection` | +| `ClearDebugShapes` | 115 | (none) | +| `SyncPlayerPreferences` | 116 | `showEntityMarkers`, `armorItemsPreferredPickupLocation`, `weaponAndToolItemsPreferredPickupLocation`, `usableItemsItemsPreferredPickupLocation`, `solidBlockItemsPreferredPickupLocation`, `miscItemsPreferredPickupLocation`, `allowNPCDetection`, `respondToHit` | +| `ClientPlaceBlock` | 117 | `position`, `rotation`, `placedBlockId` | +| `UpdateMemoriesFeatureStatus` | 118 | `isFeatureUnlocked` | +| `RemoveMapMarker` | 119 | `markerId` | + +### Inventory Packets + +| Packet | ID | Key Fields | +|--------|----|------------| +| `UpdatePlayerInventory` | 170 | `storage`, `armor`, `hotbar`, `utility`, `builderMaterial`, `tools`, `backpack`, `sortType` | +| `SetCreativeItem` | 171 | `inventorySectionId`, `slotId`, `item`, `override` | +| `DropCreativeItem` | 172 | `item` | +| `SmartGiveCreativeItem` | 173 | `item`, `moveType` | +| `DropItemStack` | 174 | `inventorySectionId`, `slotId`, `quantity` | +| `MoveItemStack` | 175 | `fromSectionId`, `fromSlotId`, `quantity`, `toSectionId`, `toSlotId` | +| `SmartMoveItemStack` | 176 | `fromSectionId`, `fromSlotId`, `quantity`, `moveType` | +| `SetActiveSlot` | 177 | `inventorySectionId`, `activeSlot` | +| `SwitchHotbarBlockSet` | 178 | `itemId` | +| `InventoryAction` | 179 | `inventorySectionId`, `inventoryActionType`, `actionData` | + +### Window Packets + +| Packet | ID | Key Fields | +|--------|----|------------| +| `OpenWindow` | 200 | `id`, `windowType`, `windowData`, `inventory`, `extraResources` | +| `UpdateWindow` | 201 | `id`, `windowData`, `inventory`, `extraResources` | +| `CloseWindow` | 202 | `id` | +| `SendWindowAction` | 203 | `id`, `action` | +| `ClientOpenWindow` | 204 | `type` | + +### Other Client Packets + +| Packet | ID | Key Fields | +|--------|----|------------| +| `ClientReferral` | 18 | `hostTo`, `data` | +| `SetUpdateRate` | 29 | `updatesPerSecond` | +| `SetTimeDilation` | 30 | `timeDilation` | +| `SetChunk` | 131 | `x`, `y`, `z`, `localLight`, `globalLight`, `data` | +| `SetChunkHeightmap` | 132 | `x`, `z`, `heightmap` | +| `SetChunkTintmap` | 133 | `x`, `z`, `tintmap` | +| `SetChunkEnvironments` | 134 | `x`, `z`, `environments` | +| `SetFluids` | 136 | `x`, `y`, `z`, `data` | +| `SetPaused` | 158 | `paused` | +| `SetEntitySeed` | 160 | `entitySeed` | +| `SetPage` | 216 | `page`, `canCloseThroughInteraction` | +| `SetServerAccess` | 252 | `access`, `password` | +| `SetMachinimaActorModel` | 261 | `model`, `sceneName`, `actorName` | +| `SetServerCamera` | 280 | `clientCameraView`, `isLocked`, `cameraSettings` | +| `SetFlyCameraMode` | 283 | `entering` | +| `SyncInteractionChains` | 290 | `updates` | + +### Packet Handlers (Server-Side) + +Packet handlers determine which packets are accepted at each phase of the connection lifecycle: + +| Handler | Packets Accepted | +|---------|-----------------| +| **InitialPacketHandler** | Connect (0), Disconnect (1) | +| **HandshakeHandler** | Disconnect (1), AuthToken (12) | +| **PasswordPacketHandler** | Disconnect (1), PasswordResponse (15) | +| **SetupPacketHandler** | Disconnect (1), RequestAssets (23), ViewRadius (32), PlayerOptions (33) | +| **GamePacketHandler** | Disconnect (1), Pong (3), ClientMovement (108), ChatMessage (211), RequestAssets (23), CustomPageEvent (219), ViewRadius (32), UpdateLanguage (232), MouseInteraction (111), SendWindowAction (203), CloseWindow (202), ClientReady (105), SyncInteractionChains (290), SetPaused (158), and more | + +**GamePacketHandler sub-handlers:** + +| Sub-Handler | Packets | +|-------------|---------| +| **InventoryPacketHandler** | SetCreativeItem (171), DropCreativeItem (172), SmartGiveCreativeItem (173), DropItemStack (174), MoveItemStack (175), SmartMoveItemStack (176), SetActiveSlot (177), SwitchHotbarBlockSet (178), InventoryAction (179) | +| **BuilderToolsPacketHandler** | LoadHotbar (106), SaveHotbar (107), BuilderToolArgUpdate (400), BuilderToolEntityAction (401), and more | +| **MountGamePacketHandler** | DismountNPC (294) | + +> If a packet arrives during the wrong connection phase, the handler disconnects the sender. + +--- + +## Part 8: Custom Camera Controls + +Camera is controlled by sending a `SetServerCamera` packet with `ServerCameraSettings` to the player. + +### Camera Imports + +```java +import com.hypixel.hytale.protocol.ClientCameraView; +import com.hypixel.hytale.protocol.Direction; +import com.hypixel.hytale.protocol.MouseInputType; +import com.hypixel.hytale.protocol.MovementForceRotationType; +import com.hypixel.hytale.protocol.PositionDistanceOffsetType; +import com.hypixel.hytale.protocol.RotationType; +import com.hypixel.hytale.protocol.ServerCameraSettings; +import com.hypixel.hytale.protocol.Vector3f; +import com.hypixel.hytale.protocol.packets.camera.SetServerCamera; +``` + +### Basic Camera Setup + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.distance = 10.0f; // Zoom distance from player +settings.isFirstPerson = false; // Third-person mode +settings.positionLerpSpeed = 0.2f; // Smooth camera follow + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +### Reset Camera to Default + +```java +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, false, null) +); +``` + +### Camera Presets + +#### Top-Down (RTS/ARPG Style) + +Source: `com.hypixel.hytale.server.core.command.commands.player.camera.PlayerCameraTopdownCommand` + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.positionLerpSpeed = 0.2f; +settings.rotationLerpSpeed = 0.2f; +settings.distance = 20.0f; +settings.displayCursor = true; +settings.isFirstPerson = false; +settings.movementForceRotationType = MovementForceRotationType.Custom; +// Align movement with camera yaw (horizontal rotation only) +settings.movementForceRotation = new Direction(-0.7853981634f, 0.0f, 0.0f); // 45° right +settings.eyeOffset = true; +settings.positionDistanceOffsetType = PositionDistanceOffsetType.DistanceOffset; +settings.rotationType = RotationType.Custom; +settings.rotation = new Direction(0.0f, -1.5707964f, 0.0f); // Look straight down +settings.mouseInputType = MouseInputType.LookAtPlane; +settings.planeNormal = new Vector3f(0.0f, 1.0f, 0.0f); // Ground plane + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +#### Side-Scroller (2D Platformer Style) + +Source: `com.hypixel.hytale.server.core.command.commands.player.camera.PlayerCameraSideScrollerCommand` + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.positionLerpSpeed = 0.2f; +settings.rotationLerpSpeed = 0.2f; +settings.distance = 15.0f; +settings.displayCursor = true; +settings.isFirstPerson = false; +settings.movementForceRotationType = MovementForceRotationType.Custom; +settings.movementMultiplier = new Vector3f(1.0f, 1.0f, 0.0f); // Lock Z-axis +settings.eyeOffset = true; +settings.positionDistanceOffsetType = PositionDistanceOffsetType.DistanceOffset; +settings.rotationType = RotationType.Custom; +settings.mouseInputType = MouseInputType.LookAtPlane; +settings.planeNormal = new Vector3f(0.0f, 0.0f, 1.0f); // Side plane + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +#### Isometric (Diablo Style) + +```java +ServerCameraSettings settings = new ServerCameraSettings(); +settings.positionLerpSpeed = 0.2f; +settings.rotationLerpSpeed = 0.2f; +settings.isFirstPerson = false; +settings.distance = 6f; +settings.allowPitchControls = false; +settings.displayCursor = true; +// Force the camera's rotation to be set by the server +settings.applyLookType = ApplyLookType.Rotation; +settings.rotationType = RotationType.Custom; + +// Set the typical isometric rotation +Direction direction = new Direction( + (float) Math.toRadians(45f), // yaw + (float) Math.toRadians(-35f), // pitch + 0f // roll +); +settings.rotation = direction; +settings.movementForceRotation = direction; + +playerRef.getPacketHandler().writeNoCache( + new SetServerCamera(ClientCameraView.Custom, true, settings) +); +``` + +### ServerCameraSettings Reference + +#### Position & Rotation + +| Setting | Description | +|---------|-------------| +| `positionLerpSpeed` (0.0-1.0) | How smoothly camera follows player. Lower = smoother but slower | +| `rotationLerpSpeed` (0.0-1.0) | How smoothly camera rotates. Lower = smoother but slower | +| `distance` | Camera distance from player. Higher = zoomed out | +| `rotation` | Camera angle as `Direction(yaw, pitch, roll)` in **radians** | +| `rotationType` | How rotation is calculated. `RotationType.Custom` uses your `rotation` value | + +#### Movement Alignment + +| Setting | Description | +|---------|-------------| +| `movementForceRotationType` | `AttachedToHead` = follows player look; `Custom` = use `movementForceRotation` | +| `movementForceRotation` | Direction for W/S movement when using `Custom`. Match yaw with camera, keep pitch at 0 | +| `movementMultiplier` | Scale movement per axis. `(1,1,0)` = lock Z-axis for 2D | + +#### Input & Display + +| Setting | Description | +|---------|-------------| +| `displayCursor` | Show/hide mouse cursor | +| `mouseInputType` | `LookAtPlane` = cursor on plane (top-down); `LookAtTarget` = rotates camera | +| `planeNormal` | For `LookAtPlane`, defines the plane. `(0,1,0)` = ground, `(0,0,1)` = side | + +#### Advanced + +| Setting | Description | +|---------|-------------| +| `positionDistanceOffsetType` | `DistanceOffset` = simple; `DistanceOffsetRaycast` = prevents wall clipping | +| `eyeOffset` | Offset camera from player's eye position | +| `isFirstPerson` | First-person vs third-person mode | +| `allowPitchControls` | Allow player to control pitch | +| `isLocked` (packet parameter) | Set `true` in `SetServerCamera` to prevent player camera changes | + +### Camera Tips + +- **Zoom:** Adjust `distance` (higher = further out) +- **Smoothness:** `positionLerpSpeed` and `rotationLerpSpeed` control camera response speed +- **Wall clipping:** Use `PositionDistanceOffsetType.DistanceOffsetRaycast` +- **Lock camera:** Set `isLocked = true` in the `SetServerCamera` packet +- **2D movement:** Set `movementMultiplier` to zero out an axis +- **Isometric cameras:** Always set `movementForceRotation` to match camera yaw +- **Angle math:** Use `Math.toRadians(degrees)` to convert degrees to radians + +--- + +## Key Warnings + +1. **Client-side prediction:** Cancelling packets does not prevent client-side visual effects. The player will still see movement/actions locally even if the server blocks the packet. +2. **Thread safety:** When accessing ECS components from packet handlers, schedule work on the world thread via `world.execute(() -> { ... })`. +3. **Packet IDs may change:** Always use `instanceof` checks or class references rather than hardcoded packet IDs when possible. The ID-based approach (`packet.getId() != 290`) is brittle across server versions. +4. **Deregister on shutdown:** Always store filter/watcher references and deregister them in your plugin's `shutdown()` method. diff --git a/skills/hytale-player-stats/SKILL.md b/skills/hytale-player-stats/SKILL.md new file mode 100644 index 0000000..15deb32 --- /dev/null +++ b/skills/hytale-player-stats/SKILL.md @@ -0,0 +1,236 @@ +--- +name: hytale-player-stats +description: Documents Hytale's player/entity stat system for reading and modifying stats like health, stamina, mana, oxygen, signature energy, and ammo using EntityStatMap and DefaultEntityStatTypes. Use when healing players, dealing damage, modifying stamina/mana, setting stat values, creating stat-related commands, or working with entity stats. Triggers - player stats, health, stamina, mana, oxygen, ammo, signature energy, EntityStatMap, DefaultEntityStatTypes, stat value, heal, damage, maximizeStatValue, subtractStatValue, addStatValue, setStatValue, resetStatValue, entity stats. +--- + +# Hytale Player Stats + +Use this skill when reading or modifying player/entity stats (health, stamina, mana, etc.) in Hytale plugins. Stats are managed through the `EntityStatMap` component and accessed via `DefaultEntityStatTypes`. + +> **Source:** + +--- + +## Quick Reference + +| Task | Code | +|------|------| +| Get stat map component | `store.getComponent(playerRef, EntityStatMap.getComponentType())` | +| Set a stat value | `statMap.setStatValue(statIndex, value)` | +| Add to a stat | `statMap.addStatValue(statIndex, amount)` | +| Subtract from a stat | `statMap.subtractStatValue(statIndex, amount)` | +| Maximize a stat (full restore) | `statMap.maximizeStatValue(statIndex)` | +| Reset a stat | `statMap.resetStatValue(statIndex)` | + +--- + +## Available Stats + +Hytale provides default stats via `DefaultEntityStatTypes`: + +| Stat | Accessor | +|------|----------| +| Health | `DefaultEntityStatTypes.getHealth()` | +| Stamina | `DefaultEntityStatTypes.getStamina()` | +| Mana | `DefaultEntityStatTypes.getMana()` | +| Oxygen | `DefaultEntityStatTypes.getOxygen()` | +| Signature Energy | `DefaultEntityStatTypes.getSignatureEnergy()` | +| Ammo | `DefaultEntityStatTypes.getAmmo()` | + +--- + +## Key Concepts + +### EntityStatMap + +`EntityStatMap` is an ECS component that holds all stat values for an entity. Retrieve it from the store: + +```java +EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); +``` + +### World Thread Safety + +All stat modifications **must** be performed on the world thread using `world.execute(() -> { ... })` to ensure thread safety. + +### Stat Indices + +Each stat type (e.g., `DefaultEntityStatTypes.getHealth()`) returns a stat index used by `EntityStatMap` methods. Pass these indices to set/add/subtract/maximize/reset operations. + +--- + +## Required Imports + +```java +import com.hypixel.hytale.server.ecs.entity.component.stats.EntityStatMap; +import com.hypixel.hytale.server.ecs.entity.component.stats.DefaultEntityStatTypes; +import com.hypixel.hytale.server.world.World; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.server.ecs.store.EntityStore; +import com.hypixel.server.ecs.store.Ref; +import com.hypixel.server.ecs.store.Store; +``` + +--- + +## EntityStatMap Methods + +| Method | Description | +|--------|-------------| +| `setStatValue(statIndex, value)` | Sets the stat to an exact value | +| `addStatValue(statIndex, amount)` | Adds to the current stat value | +| `subtractStatValue(statIndex, amount)` | Subtracts from the current stat value | +| `maximizeStatValue(statIndex)` | Restores the stat to its maximum value | +| `resetStatValue(statIndex)` | Resets the stat to its default value | + +--- + +## Access Pattern + +The standard pattern for accessing and modifying stats: + +```java +// 1. Get the player reference +Ref playerRef = /* obtain player ref */; + +// 2. Get the store and world +Store store = playerRef.getStore(); +EntityStore entityStore = store.getExternalData(); +World world = entityStore.getWorld(); + +// 3. Modify on the world thread +world.execute(() -> { + EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); + if (statMap != null) { + // Perform stat operations here + } +}); +``` + +--- + +## Example: Heal Command + +Restores the player's health to its maximum value. + +```java +public class HealCommand extends CommandBase { + public HealCommand() { + super("heal", "Restores your health to maximum."); + this.setPermissionGroup(GameMode.Adventure); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + // 1. Get the player reference + Ref playerRef = ctx.senderAsPlayerRef(); + if (playerRef == null) return; + + // 2. Get the store and the world + Store store = playerRef.getStore(); + EntityStore entityStore = store.getExternalData(); + World world = entityStore.getWorld(); + + // 3. Perform modification on the world thread + world.execute(() -> { + EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); + if (statMap != null) { + statMap.maximizeStatValue(DefaultEntityStatTypes.getHealth()); + ctx.sendMessage(Message.raw("Your health has been restored!")); + } + }); + } +} +``` + +--- + +## Example: Damage Self Command + +Removes a specified amount of health from the player. + +```java +public class DamageSelfCommand extends CommandBase { + private final Argument amountArg; + + public DamageSelfCommand() { + super("damageself", "Damages yourself by a specific amount."); + this.setPermissionGroup(GameMode.Adventure); + this.amountArg = this.withRequiredArg("amount", "Amount of damage", ArgTypes.FLOAT); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + // 1. Get the player reference + Ref playerRef = ctx.senderAsPlayerRef(); + if (playerRef == null) return; + + // 2. Get command arg + Float amount = (Float) this.amountArg.get(ctx); + if (amount == null) return; + + // 3. Get the store and the world + Store store = playerRef.getStore(); + EntityStore entityStore = store.getExternalData(); + World world = entityStore.getWorld(); + + // 4. Perform modification on the world thread + world.execute(() -> { + EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); + if (statMap != null) { + statMap.subtractStatValue(DefaultEntityStatTypes.getHealth(), amount); + ctx.sendMessage(Message.raw("Ouch! You took " + amount + " damage.")); + } + }); + } +} +``` + +--- + +## Common Patterns + +### Restore All Stats + +```java +world.execute(() -> { + EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); + if (statMap != null) { + statMap.maximizeStatValue(DefaultEntityStatTypes.getHealth()); + statMap.maximizeStatValue(DefaultEntityStatTypes.getStamina()); + statMap.maximizeStatValue(DefaultEntityStatTypes.getMana()); + statMap.maximizeStatValue(DefaultEntityStatTypes.getOxygen()); + } +}); +``` + +### Set Stat to Specific Value + +```java +world.execute(() -> { + EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); + if (statMap != null) { + statMap.setStatValue(DefaultEntityStatTypes.getHealth(), 50.0f); + } +}); +``` + +### Add to a Stat (Partial Heal) + +```java +world.execute(() -> { + EntityStatMap statMap = (EntityStatMap) store.getComponent(playerRef, EntityStatMap.getComponentType()); + if (statMap != null) { + statMap.addStatValue(DefaultEntityStatTypes.getHealth(), 20.0f); + } +}); +``` + +--- + +## Important Notes + +- Always null-check `playerRef` and `statMap` before use. +- Always wrap stat modifications in `world.execute(() -> { ... })` for thread safety. +- `maximizeStatValue` restores to the entity's configured maximum, not a hardcoded value. +- These APIs work on any entity with an `EntityStatMap` component, not just players. diff --git a/skills/hytale-playing-sounds/SKILL.md b/skills/hytale-playing-sounds/SKILL.md new file mode 100644 index 0000000..7d0a8af --- /dev/null +++ b/skills/hytale-playing-sounds/SKILL.md @@ -0,0 +1,311 @@ +--- +name: hytale-playing-sounds +description: Plays sounds to players in Hytale plugins using SoundUtil and SoundEvent. Use when playing sound effects, music, ambient audio, UI sounds, or any positional 3D audio to players. Triggers - sound, play sound, SoundUtil, SoundEvent, SoundCategory, audio, SFX, music, ambient, playSoundEvent3dToPlayer, sound index, 3D sound. +--- + +# Hytale Playing Sounds Skill + +Use this skill when playing sounds to players in Hytale plugins. Sounds are played using `SoundUtil` with a positional `TransformComponent` and a sound index resolved from the `SoundEvent` asset map. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **SoundEvent** | Asset map for resolving sound IDs to numeric indexes | +| **SoundUtil** | Utility class for playing sounds to players | +| **SoundCategory** | Classifies sound type (`SFX`, `UI`, `Music`, `Ambient`) | +| **TransformComponent** | Provides the 3D position where the sound plays | +| **World.execute()** | Required thread-safe execution context for sound playback | + +--- + +## Sound Playback Flow + +1. **Resolve the sound index** from `SoundEvent.getAssetMap()` +2. **Get the player reference** (`Ref`) +3. **Get the world** and entity store +4. **Execute on the world thread** via `world.execute()` +5. **Get the TransformComponent** for the sound position +6. **Play the sound** via `SoundUtil.playSoundEvent3dToPlayer()` + +--- + +## Required Imports + +```java +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.EntityModule; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.sound.SoundCategory; +import com.hypixel.hytale.server.sound.SoundEvent; +import com.hypixel.hytale.server.util.SoundUtil; +import com.hypixel.hytale.store.Ref; +``` + +--- + +## Sound Indexes + +Sounds are referenced by numeric index, resolved from the `SoundEvent` asset map using the sound's string key: + +```java +int index = SoundEvent.getAssetMap().getIndex("SFX_Cactus_Large_Hit"); +``` + +See the [full list of available sounds](https://hytalemodding.dev/en/docs/server/sounds) for valid sound keys. + +--- + +## Sound Categories + +`SoundCategory` classifies the type of sound being played: + +| Category | Use Case | +|----------|----------| +| `SoundCategory.SFX` | Sound effects (combat, interactions, impacts) | +| `SoundCategory.UI` | User interface sounds (clicks, notifications) | +| `SoundCategory.Music` | Background music | +| `SoundCategory.Ambient` | Environmental/ambient audio | + +--- + +## Getting the TransformComponent + +The `TransformComponent` determines the 3D position of the sound. Two approaches: + +### From the Player (Recommended) + +Plays the sound at the player's current position: + +```java +TransformComponent transform = store.getStore().getComponent( + playerRef, + EntityModule.get().getTransformComponentType() +); +``` + +### From a Custom Position + +Plays the sound at an arbitrary world position (player must be close enough to hear): + +```java +Vector3d position = new Vector3d(100, 64, 200); +Vector3f rotation = new Vector3f(0, 0, 0); +TransformComponent transform = new TransformComponent(position, rotation); +``` + +--- + +## Basic Example + +Play a sound to a player at their current position: + +```java +public void playSound(Player player) { + int index = SoundEvent.getAssetMap().getIndex("SFX_Cactus_Large_Hit"); + World world = player.getWorld(); + EntityStore store = world.getEntityStore(); + Ref playerRef = player.getReference(); + + world.execute(() -> { + TransformComponent transform = store.getStore().getComponent( + playerRef, + EntityModule.get().getTransformComponentType() + ); + + SoundUtil.playSoundEvent3dToPlayer( + playerRef, + index, + SoundCategory.UI, + transform.getPosition(), + store.getStore() + ); + }); +} +``` + +--- + +## Common Use Cases + +### Play a UI Sound on Event + +```java +public void onPlayerAction(Player player) { + int soundIndex = SoundEvent.getAssetMap().getIndex("SFX_UI_Click"); + World world = player.getWorld(); + EntityStore store = world.getEntityStore(); + Ref playerRef = player.getReference(); + + world.execute(() -> { + TransformComponent transform = store.getStore().getComponent( + playerRef, + EntityModule.get().getTransformComponentType() + ); + + SoundUtil.playSoundEvent3dToPlayer( + playerRef, + soundIndex, + SoundCategory.UI, + transform.getPosition(), + store.getStore() + ); + }); +} +``` + +### Play a Sound at a Specific Location + +```java +public void playSoundAtPosition(Player player, double x, double y, double z, String soundKey) { + int soundIndex = SoundEvent.getAssetMap().getIndex(soundKey); + World world = player.getWorld(); + EntityStore store = world.getEntityStore(); + Ref playerRef = player.getReference(); + + world.execute(() -> { + Vector3d position = new Vector3d(x, y, z); + Vector3f rotation = new Vector3f(0, 0, 0); + TransformComponent transform = new TransformComponent(position, rotation); + + SoundUtil.playSoundEvent3dToPlayer( + playerRef, + soundIndex, + SoundCategory.SFX, + transform.getPosition(), + store.getStore() + ); + }); +} +``` + +### Play a Combat SFX + +```java +public void playCombatSound(Player player, String soundKey) { + int soundIndex = SoundEvent.getAssetMap().getIndex(soundKey); + World world = player.getWorld(); + EntityStore store = world.getEntityStore(); + Ref playerRef = player.getReference(); + + world.execute(() -> { + TransformComponent transform = store.getStore().getComponent( + playerRef, + EntityModule.get().getTransformComponentType() + ); + + SoundUtil.playSoundEvent3dToPlayer( + playerRef, + soundIndex, + SoundCategory.SFX, + transform.getPosition(), + store.getStore() + ); + }); +} +``` + +--- + +## Utility Wrapper Class + +Consider creating a utility wrapper for consistent sound playback: + +```java +public class Sounds { + + public static void playToPlayer(Player player, String soundKey, SoundCategory category) { + int index = SoundEvent.getAssetMap().getIndex(soundKey); + World world = player.getWorld(); + EntityStore store = world.getEntityStore(); + Ref playerRef = player.getReference(); + + world.execute(() -> { + TransformComponent transform = store.getStore().getComponent( + playerRef, + EntityModule.get().getTransformComponentType() + ); + + SoundUtil.playSoundEvent3dToPlayer( + playerRef, + index, + category, + transform.getPosition(), + store.getStore() + ); + }); + } + + public static void playAtPosition(Player player, String soundKey, SoundCategory category, + double x, double y, double z) { + int index = SoundEvent.getAssetMap().getIndex(soundKey); + World world = player.getWorld(); + EntityStore store = world.getEntityStore(); + Ref playerRef = player.getReference(); + + world.execute(() -> { + Vector3d position = new Vector3d(x, y, z); + Vector3f rotation = new Vector3f(0, 0, 0); + TransformComponent transform = new TransformComponent(position, rotation); + + SoundUtil.playSoundEvent3dToPlayer( + playerRef, + index, + category, + transform.getPosition(), + store.getStore() + ); + }); + } + + public static void playSfx(Player player, String soundKey) { + playToPlayer(player, soundKey, SoundCategory.SFX); + } + + public static void playUi(Player player, String soundKey) { + playToPlayer(player, soundKey, SoundCategory.UI); + } +} +``` + +--- + +## Best Practices + +1. **Always use `world.execute()`**: Sound playback must run on the world thread for thread safety +2. **Prefer player transform**: Getting the transform from the player ensures they hear the sound; custom positions risk being out of audible range +3. **Choose the correct SoundCategory**: This affects volume mixing and player audio settings +4. **Cache sound indexes**: If playing the same sound frequently, resolve the index once and reuse it +5. **Validate sound keys**: Ensure the sound key exists in the `SoundEvent` asset map before playing +6. **Don't spam sounds**: Avoid playing many sounds in rapid succession as it can overwhelm the client audio + +--- + +## playSoundEvent3dToPlayer Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `playerRef` | `Ref` | The target player reference | +| `soundIndex` | `int` | Numeric index from `SoundEvent.getAssetMap()` | +| `category` | `SoundCategory` | Sound classification (`SFX`, `UI`, `Music`, `Ambient`) | +| `position` | `Vector3d` | 3D world position of the sound source | +| `store` | `Store` | The entity store instance | + +--- + +## Related APIs + +- [Notifications](../hytale-notifications/SKILL.md) - For visual notifications with sounds +- [Text Holograms](../hytale-text-holograms/SKILL.md) - For visual world elements +- [ECS](../hytale-ecs/SKILL.md) - Entity Component System patterns + +--- + +## References + +- [Official Documentation](https://hytalemodding.dev/en/docs/guides/plugin/playing-sounds) +- [Available Sounds List](https://hytalemodding.dev/en/docs/server/sounds) +``` diff --git a/skills/hytale-plugin-config/SKILL.md b/skills/hytale-plugin-config/SKILL.md new file mode 100644 index 0000000..6461bce --- /dev/null +++ b/skills/hytale-plugin-config/SKILL.md @@ -0,0 +1,295 @@ +--- +name: hytale-plugin-config +description: Creates and manages plugin configuration files in Hytale using Config, BuilderCodec, and withConfig(). Use when adding plugin settings, creating config classes, loading/saving config files, or accessing config data from other classes. Triggers - config, configuration, plugin config, Config, withConfig, plugin settings, config file, save config, load config, configuration class, plugin options. +--- + +# Hytale Plugin Configuration Files + +This skill documents how to create, load, save, and use configuration files in Hytale plugins using the `Config` API. + +> **Related skills:** For Codec/BuilderCodec serialization details, see `hytale-persistent-data`. For ECS component registration, see `hytale-ecs`. + +## Quick Reference + +| Task | Approach | +|------|----------| +| Define config class | Plain class with `BuilderCodec` and default constructor | +| Register config | `this.withConfig("Name", MyConfig.CODEC)` in plugin field initializer | +| Save config to file | `config.save()` in `setup()` (creates file if missing) | +| Read config value | `config.get().getSomeValue()` | +| Modify config value | `config.get().setSomeValue(newVal)` then `config.save()` | +| Config file location | Server `mods/` folder | + +--- + +## Configuration Class + +A configuration class is a plain Java class (NOT an ECS component) with a `BuilderCodec` that defines how each field serializes/deserializes. + +### Critical Rules + +1. **Codec keys MUST be capitalized** — lowercase keys throw an error at load time. +2. **Must have a default (no-arg) constructor** — the codec uses it as a factory. +3. **Does NOT implement `Component`** — config classes are not ECS components. + +### Template + +```java +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +public class MyConfig { + + // === Codec Definition === + // Keys MUST be capitalized (e.g. "SomeValue", not "someValue") + public static final BuilderCodec CODEC = + BuilderCodec.builder(MyConfig.class, MyConfig::new) + .append(new KeyedCodec("SomeValue", Codec.INTEGER), + (config, value) -> config.someValue = value, // setter + (config) -> config.someValue) // getter + .add() + .append(new KeyedCodec("SomeString", Codec.STRING), + (config, value) -> config.someString = value, + (config) -> config.someString) + .add() + .build(); + + // === Fields with defaults === + private int someValue = 12; + private String someString = "My default string"; + + // === Default Constructor (required) === + public MyConfig() { + } + + // === Getters === + public int getSomeValue() { + return someValue; + } + + public String getSomeString() { + return someString; + } + + // === Setters === + public void setSomeValue(int someValue) { + this.someValue = someValue; + } + + public void setSomeString(String someString) { + this.someString = someString; + } +} +``` + +--- + +## Registering and Loading the Config + +### Rules + +- `withConfig(...)` **MUST** be called as a field initializer (or in the constructor) — calling it after `setup()` throws an error. +- Call `config.save()` in `setup()` to create the config file on disk if it doesn't already exist. + +### Plugin Setup Template + +```java +import com.hypixel.hytale.server.plugin.Config; +import com.hypixel.hytale.server.plugin.JavaPlugin; + +public class ExamplePlugin extends JavaPlugin { + + // Register config — MUST be a field initializer (before setup()) + private final Config config = this.withConfig("MyConfig", MyConfig.CODEC); + + @Override + public void setup() { + // Ensures the config file is created if it doesn't exist + config.save(); + } +} +``` + +The first argument to `withConfig()` is the config file name (without extension). The file is created in the server's `mods/` folder. + +--- + +## Accessing Config Values + +### From the Plugin Class + +```java +public void someMethod() { + MyConfig myConfig = config.get(); + int value = myConfig.getSomeValue(); + String str = myConfig.getSomeString(); +} +``` + +### From Other Classes + +Pass the plugin instance or expose a getter: + +**Option A — Pass plugin reference:** + +```java +public class SomeOtherClass { + private final ExamplePlugin plugin; + + public SomeOtherClass(ExamplePlugin plugin) { + this.plugin = plugin; + } + + public void someMethod() { + MyConfig myConfig = plugin.getConfig().get(); + int value = myConfig.getSomeValue(); + String str = myConfig.getSomeString(); + } +} +``` + +**Option B — Expose a typed getter on the plugin:** + +```java +// In ExamplePlugin +public Config getPluginConfig() { + return config; +} +``` + +--- + +## Modifying and Saving Config + +Changes to config values persist across server restarts when saved: + +```java +MyConfig myConfig = config.get(); + +// Modify values +myConfig.setSomeValue(999); +myConfig.setSomeString("A new string"); + +// Save changes back to disk +config.save(); +``` + +--- + +## Common Codec Types for Config Fields + +Use these `Codec` types in `KeyedCodec` for your config fields: + +| Java Type | Codec | Example Key | +|-----------|-------|-------------| +| `int` | `Codec.INTEGER` | `"MaxPlayers"` | +| `float` | `Codec.FLOAT` | `"SpawnRate"` | +| `double` | `Codec.DOUBLE` | `"DamageMultiplier"` | +| `boolean` | `Codec.BOOLEAN` | `"EnableFeature"` | +| `String` | `Codec.STRING` | `"WelcomeMessage"` | +| `long` | `Codec.LONG` | `"CooldownMs"` | +| `List` | `ListCodec` | `"AllowedWorlds"` | +| `Map` | `MapCodec` | `"LevelThresholds"` | +| `Set` | `SetCodec` | `"BannedItems"` | + +> For advanced codec usage (nested objects, collections, validators), see the `hytale-persistent-data` skill. + +--- + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Lowercase codec key (`"someValue"`) | **Capitalize:** `"SomeValue"` | +| Calling `withConfig()` inside `setup()` | **Move to field initializer** or constructor | +| Forgetting `config.save()` in `setup()` | Config file won't be created on first run | +| Forgetting `.add()` after `.append(...)` | Codec builder chain is incomplete | +| Forgetting `.build()` at end of codec | Codec is not finalized | +| Not saving after modifying values | Changes are lost on restart — call `config.save()` | + +--- + +## Complete Example + +### Config Class + +```java +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +public class ServerConfig { + + public static final BuilderCodec CODEC = + BuilderCodec.builder(ServerConfig.class, ServerConfig::new) + .append(new KeyedCodec("MaxPlayers", Codec.INTEGER), + (c, v) -> c.maxPlayers = v, + c -> c.maxPlayers) + .add() + .append(new KeyedCodec("PvpEnabled", Codec.BOOLEAN), + (c, v) -> c.pvpEnabled = v, + c -> c.pvpEnabled) + .add() + .append(new KeyedCodec("Motd", Codec.STRING), + (c, v) -> c.motd = v, + c -> c.motd) + .add() + .append(new KeyedCodec("DamageMultiplier", Codec.DOUBLE), + (c, v) -> c.damageMultiplier = v, + c -> c.damageMultiplier) + .add() + .build(); + + private int maxPlayers = 20; + private boolean pvpEnabled = true; + private String motd = "Welcome to the server!"; + private double damageMultiplier = 1.0; + + public ServerConfig() { + } + + public int getMaxPlayers() { return maxPlayers; } + public void setMaxPlayers(int maxPlayers) { this.maxPlayers = maxPlayers; } + + public boolean isPvpEnabled() { return pvpEnabled; } + public void setPvpEnabled(boolean pvpEnabled) { this.pvpEnabled = pvpEnabled; } + + public String getMotd() { return motd; } + public void setMotd(String motd) { this.motd = motd; } + + public double getDamageMultiplier() { return damageMultiplier; } + public void setDamageMultiplier(double damageMultiplier) { this.damageMultiplier = damageMultiplier; } +} +``` + +### Plugin Class + +```java +import com.hypixel.hytale.server.plugin.Config; +import com.hypixel.hytale.server.plugin.JavaPlugin; + +public class MyPlugin extends JavaPlugin { + + private final Config config = this.withConfig("ServerConfig", ServerConfig.CODEC); + + @Override + public void setup() { + config.save(); + } + + @Override + public void start() { + ServerConfig cfg = config.get(); + getLogger().info("Max players: " + cfg.getMaxPlayers()); + getLogger().info("PvP enabled: " + cfg.isPvpEnabled()); + getLogger().info("MOTD: " + cfg.getMotd()); + getLogger().info("Damage multiplier: " + cfg.getDamageMultiplier()); + } + + public Config getServerConfig() { + return config; + } +} +``` +``` diff --git a/skills/hytale-prefabs/SKILL.md b/skills/hytale-prefabs/SKILL.md new file mode 100644 index 0000000..e762405 --- /dev/null +++ b/skills/hytale-prefabs/SKILL.md @@ -0,0 +1,102 @@ +--- +name: hytale-prefabs +description: Documents Hytale's prefab system for creating, saving, loading, and managing reusable structures via in-game commands. Use when creating prefabs, saving structures, loading prefabs, editing prefab worlds, or working with reusable structures. Triggers - prefab, prefab system, /prefab, /editprefab, reusable structure, structure, save structure, load structure, prefab world, prefab editing, prefab commands, paste brush, selection brush. +--- + +# Hytale Prefabs + +Reference for Hytale's prefab system — creating, saving, loading, and managing reusable structures via in-game commands. + +> **Source:** +> **Related skills:** For spawning entities within prefabs, see `hytale-spawning-entities`. For world generation with prefabs, see `hytale-world-gen`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Create a new prefab world | `/editprefab new ` | +| Save a prefab | `/prefab save` (use selection brush first) | +| Load a prefab | `/prefab load` | +| List all prefabs | `/prefab list` | +| Delete a prefab | `/prefab delete` | +| Exit prefab editing world | `/editprefab exit` | +| Select a prefab area | Use the selection brush in the editing world | +| Paste a prefab | Use the Paste brush, press `E` to select from menu | +| See command options | Append `--help` to any command (e.g., `/prefab save --help`) | + +--- + +## Key Concepts + +- **Prefab editing world** — A dedicated world created for building and editing prefabs. Created with `/editprefab new`. +- **Prefabs** — Physical structures saved as JSON files. A single editing world can contain multiple prefabs. +- **Selection brush** — In-game tool used to select the area to save as a prefab. +- **Paste brush** — In-game tool used to place saved prefabs. Press `E` to open the selection menu. + +--- + +## Basic Workflow + +1. `/editprefab new my_prefab_world` — Create a new prefab editing world +2. Build the structure you want in the editing world +3. Use the **selection brush** to select the area +4. `/prefab save` — Save the selected area as a prefab +5. `/editprefab exit` — Exit the editing world +6. Use the **Paste brush**, press `E` to select the prefab from the "server" dropdown (top-right of menu) + +--- + +## Commands + +### `/prefab` + +Manages prefab files on disk. + +| Subcommand | Description | +|------------|-------------| +| `save` | Saves the prefab to the file system | +| `load` | Loads a prefab into the game | +| `delete` | Deletes the prefab from the file system | +| `list` | Lists all prefabs in the file system | + +### `/editprefab` + +Manages the physical structure editing workflow. + +| Subcommand | Description | +|------------|-------------| +| `new` | Creates a new prefab editing world from scratch | +| `load` | Creates a new editing world with an existing prefab pasted in | +| `exit` | Exits the current prefab editing world | +| `select` | Selects the prefab area the user is looking at (within 200 blocks) | +| `save` | Saves the current prefab using the existing or selected area | +| `saveui` | Opens the save UI for managing all prefabs in the current world | +| `saveas` | Saves the selected prefab into a new file | +| `kill` | Despawns all entities in the currently selected prefab | +| `setbox` | Sets the bounding box of the currently selected prefab | +| `info` | Shows information about the currently selected prefab | +| `tp` | Opens teleport UI to jump to a prefab in the current editing world | +| `modified` | Lists all modified prefabs with unsaved changes | + +> Use `--help` at the end of any command to see all available options (e.g., `/editprefab save --help`). + +--- + +## Known Issues + +- After saving edits to an existing prefab, the prefab may not reflect changes immediately. **Workaround:** exit and re-enter the world. +- When pasting a prefab, it may not always display accurately. **Workaround:** press `T` to toggle the material view; usually at least one view is correct. +- `/prefab delete` may error with `Assert not in thread`. + +--- + +## Edge Cases & Gotchas + +- The world name in `/editprefab new ` is the *world* name, not the prefab name. Worlds can contain multiple prefabs. +- Saved prefabs appear in the "server" dropdown of the Paste brush menu (top-right). +- Prefabs are saved as JSON files to the server file system. +- This is primarily a command-based workflow — there is currently no public Java API for programmatic prefab manipulation from plugins. + +``` diff --git a/skills/hytale-spawning-entities/SKILL.md b/skills/hytale-spawning-entities/SKILL.md new file mode 100644 index 0000000..e43365f --- /dev/null +++ b/skills/hytale-spawning-entities/SKILL.md @@ -0,0 +1,330 @@ +--- +name: hytale-spawning-entities +description: Spawns visible entities with models in Hytale plugins using Holder, ModelAsset, and Store. Use when spawning entities, creating model-based entities, placing entities in the world, or making entities persist across saves. Triggers - spawn entity, entity spawn, ModelAsset, Model, PersistentModel, ModelComponent, BoundingBox, Holder, addEntity, AddReason, SPAWN, createScaledModel, Interactable, Interactions, PropComponent, HolderSystem, visible entity, world.execute, entity model. +--- + +# Hytale Spawning Entities + +Use this skill when spawning visible, model-based entities in the world. Covers getting the World, creating entity Holders, attaching models, adding all required components, spawning, and persisting entities across server saves. + +> **Related skills:** For ECS fundamentals (Components, Systems, Queries), see `hytale-ecs`. For invisible text-only entities, see `hytale-text-holograms`. For NPC spawning helpers, see the [Spawning NPCs guide](https://hytalemodding.dev/en/docs/guides/plugin/spawning-npcs). + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| Get World from player | `player.getWorld()` | +| Get World by UUID | `Universe.get().getWorld("uuid")` | +| Get entity store | `world.getEntityStore().getStore()` | +| Create entity holder | `EntityStore.REGISTRY.newHolder()` | +| Get a model asset | `ModelAsset.getAssetMap().getAsset("EntityName")` | +| Create scaled model | `Model.createScaledModel(modelAsset, scale)` | +| Set entity position | `new TransformComponent(position, rotation)` | +| Get position from player | `store.getComponent(playerRef.getReference(), EntityModule.get().getTransformComponentType())` | +| Add entity to world | `store.addEntity(holder, AddReason.SPAWN)` | +| Run on world thread | `world.execute(() -> { ... })` | + +--- + +## Prerequisites + +Familiarize yourself with Hytale's Entity Component System (ECS) before proceeding. See the `hytale-ecs` skill for full reference. + +Your plugin `manifest.json` must declare these dependencies: + +```json +{ + "dependencies": ["Hytale:EntityModule", "Hytale:BlockModule"] +} +``` + +--- + +## Step-by-Step: Spawning an Entity + +### 1. Get the World Object + +You can retrieve the `World` from a player or by UUID: + +```java +// From a Player object +World world = player.getWorld(); + +// From Universe by world UUID +World world = Universe.get().getWorld("your-world-uuid"); +``` + +### 2. Get the EntityStore + +```java +Store store = world.getEntityStore().getStore(); +``` + +### 3. Execute on World Thread + +**All entity mutations must run on the world thread.** Use `world.execute()`: + +```java +world.execute(() -> { + // All entity creation and spawning code goes here +}); +``` + +### 4. Create a Holder + +A `Holder` is a staging buffer — you assemble all components on it, then add the complete entity to the store. + +```java +Holder holder = EntityStore.REGISTRY.newHolder(); +``` + +### 5. Get a Model + +Look up a model by its asset name (see [entity list](https://hytalemodding.dev/en/docs/server/entities) for available names): + +```java +ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset("Minecart"); +Model model = Model.createScaledModel(modelAsset, 1.0f); +``` + +### 6. Create a TransformComponent (Position) + +Choose one of these approaches: + +**From a player's current position:** + +```java +// Get the player's Ref first +Ref playerRef = /* player's entity ref */; +TransformComponent transform = store.getComponent( + playerRef.getReference(), + EntityModule.get().getTransformComponentType() +); +``` + +**From explicit coordinates:** + +```java +Vector3d position = new Vector3d(0, 0, 0); // world position +Vector3f rotation = new Vector3f(0, 0, 0); // rotation +TransformComponent transform = new TransformComponent(position, rotation); +``` + +### 7. Add Components to the Holder + +Add the core visual and identity components: + +```java +// Position +holder.addComponent( + TransformComponent.getComponentType(), + new TransformComponent(position, new Vector3f(0, 0, 0)) +); + +// Model (visual appearance) +holder.addComponent( + PersistentModel.getComponentType(), + new PersistentModel(model.toReference()) +); +holder.addComponent( + ModelComponent.getComponentType(), + new ModelComponent(model) +); + +// Collision bounds +holder.addComponent( + BoundingBox.getComponentType(), + new BoundingBox(model.getBoundingBox()) +); + +// Network sync (required for clients to see the entity) +holder.addComponent( + NetworkId.getComponentType(), + new NetworkId(store.getExternalData().takeNextNetworkId()) +); + +// Interactions (needed if entity should be interactable) +holder.addComponent( + Interactions.getComponentType(), + new Interactions() +); +``` + +### 8. Ensure Required Base Components + +Hytale expects certain "default" components on all entities: + +```java +holder.ensureComponent(UUIDComponent.getComponentType()); +holder.ensureComponent(Interactable.getComponentType()); // if interactable +``` + +### 9. Spawn the Entity + +```java +store.addEntity(holder, AddReason.SPAWN); +``` + +--- + +## Complete Example + +```java +world.execute(() -> { + Store store = world.getEntityStore().getStore(); + Holder holder = EntityStore.REGISTRY.newHolder(); + + // Model + ModelAsset modelAsset = ModelAsset.getAssetMap().getAsset("Minecart"); + Model model = Model.createScaledModel(modelAsset, 1.0f); + + // Position + Vector3d position = new Vector3d(100, 65, 200); + + // Add components + holder.addComponent( + TransformComponent.getComponentType(), + new TransformComponent(position, new Vector3f(0, 0, 0)) + ); + holder.addComponent( + PersistentModel.getComponentType(), + new PersistentModel(model.toReference()) + ); + holder.addComponent( + ModelComponent.getComponentType(), + new ModelComponent(model) + ); + holder.addComponent( + BoundingBox.getComponentType(), + new BoundingBox(model.getBoundingBox()) + ); + holder.addComponent( + NetworkId.getComponentType(), + new NetworkId(store.getExternalData().takeNextNetworkId()) + ); + holder.addComponent( + Interactions.getComponentType(), + new Interactions() + ); + + // Base components + holder.ensureComponent(UUIDComponent.getComponentType()); + holder.ensureComponent(Interactable.getComponentType()); + + // Spawn + store.addEntity(holder, AddReason.SPAWN); +}); +``` + +--- + +## Required Components Summary + +| Component | Purpose | Required? | +|-----------|---------|-----------| +| `TransformComponent` | Position and rotation in the world | Yes | +| `PersistentModel` | Model reference for serialization/persistence | Yes | +| `ModelComponent` | Model instance for rendering | Yes | +| `BoundingBox` | Collision bounds derived from model | Yes | +| `NetworkId` | Client synchronization (entity visible to players) | Yes | +| `UUIDComponent` | Unique entity identity | Yes (use `ensureComponent`) | +| `Interactions` | Interaction handler container | If interactable | +| `Interactable` | Marks entity as interactable | If interactable | + +--- + +## Making Entities Persist + +Hytale automatically saves all entities, but `NetworkId` (required for clients to see the entity) must be **re-added every time the entity is loaded** from saved data. There are two approaches: + +### Option A: HolderSystem (Recommended) + +Create a system that re-adds `NetworkId` when entities with your custom component are loaded: + +```java +public class AddNetworkIdToMyEntitySystem extends HolderSystem { + + private final ComponentType myEntityComponentType = + MyEntityComponent.getComponentType(); + private final ComponentType networkIdComponentType = + NetworkId.getComponentType(); + private final Query query = + Query.and(this.myEntityComponentType, Query.not(this.networkIdComponentType)); + + @Override + public void onEntityAdd( + @NotNull Holder holder, + @NotNull AddReason reason, + @NotNull Store store) { + if (!holder.getArchetype().contains(NetworkId.getComponentType())) { + holder.addComponent( + NetworkId.getComponentType(), + new NetworkId(store.getExternalData().takeNextNetworkId()) + ); + } + } + + @Override + public void onEntityRemoved( + @NotNull Holder holder, + @NotNull RemoveReason reason, + @NotNull Store store) { + // No-op + } + + @Override + public @Nullable Query getQuery() { + return query; + } +} +``` + +Register the system in your plugin's `setup()`: + +```java +this.getEntityStoreRegistry().registerSystem(new AddNetworkIdToMyEntitySystem()); +``` + +### Option B: PropComponent + +Add `PropComponent` to your entity. Hytale uses this for entities spawned with the Entity Tool — it automatically gets `NetworkId` and `PrefabCopyableComponent` on load. + +```java +holder.addComponent(PropComponent.getComponentType(), new PropComponent()); +``` + +> **Warning:** Hytale may add additional behavior to `PropComponent` in future versions. This could cause unexpected side effects. The HolderSystem approach is safer for custom entities. + +--- + +## Key Points + +1. **World Thread Required** — All entity creation/mutation must run inside `world.execute(() -> { ... })` +2. **NetworkId Required** — Without it, clients cannot see the entity +3. **UUIDComponent Required** — Provides unique entity identity; use `ensureComponent` to auto-generate +4. **Model Lookup** — Use `ModelAsset.getAssetMap().getAsset("Name")` to find existing entity models +5. **Persistence** — `NetworkId` must be re-added on every load; use a `HolderSystem` or `PropComponent` +6. **Holder Pattern** — Always stage all components on a `Holder` before calling `store.addEntity()` + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Entity not visible | Ensure `NetworkId` is added with a valid ID from `takeNextNetworkId()` | +| Entity not spawning | Verify code runs inside `world.execute()` | +| Wrong position | Check `TransformComponent` coordinates; Y is vertical | +| Entity disappears on reload | Implement `HolderSystem` to re-add `NetworkId` on load | +| Model not found | Verify asset name in [entity list](https://hytalemodding.dev/en/docs/server/entities); names are case-sensitive | +| Cannot interact with entity | Add both `Interactions` and `Interactable` components | + +--- + +## Reference + +- Source: [Hytale Modding — Spawning Entities Guide](https://hytalemodding.dev/en/docs/guides/plugin/spawning-entities) +- Related: [Spawning NPCs](https://hytalemodding.dev/en/docs/guides/plugin/spawning-npcs) +- Related: [Entity List](https://hytalemodding.dev/en/docs/server/entities) diff --git a/skills/hytale-spawning-npcs/SKILL.md b/skills/hytale-spawning-npcs/SKILL.md new file mode 100644 index 0000000..119dff8 --- /dev/null +++ b/skills/hytale-spawning-npcs/SKILL.md @@ -0,0 +1,265 @@ +--- +name: hytale-spawning-npcs +description: Spawns NPCs in Hytale plugins using the NPCPlugin helper. Use when spawning NPCs, equipping NPC inventory, setting NPC armor, creating NPC commands, or working with INonPlayerCharacter. Triggers - NPC, spawn NPC, NPCPlugin, INonPlayerCharacter, NPCEntity, NPC inventory, NPC armor, Kweebec, spawnNPC, NPC command. +--- + +# Hytale Spawning NPCs Skill + +Use this skill when spawning Non-Player Characters (NPCs) using the `NPCPlugin` helper. This is the convenient alternative to manually spawning entities with `Holder`. + +> Prerequisite: Familiarity with the Entity Component System (ECS). See the `hytale-ecs` skill. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **NPCPlugin** | Helper class that simplifies NPC spawning | +| **INonPlayerCharacter** | Interface providing NPC-specific methods | +| **NPCEntity** | Component for accessing NPC inventory and settings | +| **Ref\** | ECS reference to the spawned NPC entity | +| **InventoryHelper** | Hytale API utility for equipping armor | +| **Inventory** | Object for managing NPC hotbar, armor, and items | + +--- + +## Required Imports + +```java +import com.example.npc.NPCPlugin; // Adjust import as necessary +import hytale.server.plugin.npc.INonPlayerCharacter; +import hytale.server.plugin.npc.NPCEntity; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.InventoryHelper; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import org.apache.commons.lang3.tuple.Pair; +import java.util.Objects; +``` + +--- + +## Step-by-Step Process + +### 1. Spawn the NPC + +Use `NPCPlugin.get().spawnNPC(...)` to create the entity, assign its model, and position it in the world. + +```java +Pair, INonPlayerCharacter> result = NPCPlugin.get().spawnNPC( + store, // The entity store where the NPC will exist + "Kweebec_Sapling", // The key/name of the entity model/type + null, // Optional configuration (null for defaults) + position, // Vec3d position to spawn at + rotation // Vec3f facing direction +); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `store` | `Store` | The entity store where the NPC will exist | +| `entityKey` | `String` | The key/name of the entity model/type (e.g., `"Kweebec_Sapling"`) | +| `config` | `Object` | Optional configuration, pass `null` for defaults | +| `position` | `Vector3d` | World position to spawn the NPC | +| `rotation` | `Vector3f` | Facing direction of the NPC | + +### 2. Handle the Result + +The method returns a `Pair`. Always check for `null` to confirm the spawn succeeded. + +```java +if (result != null) { + Ref npcRef = result.first(); // ECS entity reference + INonPlayerCharacter npc = result.second(); // NPC-specific interface + + // Proceed with customization... + setupNPCInventory(npcRef, store); +} +``` + +| Return | Type | Description | +|--------|------|-------------| +| `result.first()` | `Ref` | ECS reference to add components or modify the entity | +| `result.second()` | `INonPlayerCharacter` | NPC-specific methods interface | + +### 3. Access the NPC Inventory + +Retrieve the `NPCEntity` component to access inventory settings: + +```java +NPCEntity npcComponent = store.getComponent( + npcRef, + Objects.requireNonNull(NPCEntity.getComponentType()) +); + +// Initialize inventory size (rows, columns, offset) +npcComponent.setInventorySize(3, 9, 0); +``` + +### 4. Add Items and Armor + +Use the `Inventory` object to equip items and armor: + +```java +Inventory inventory = npcComponent.getInventory(); + +// Add a weapon to the first hotbar slot +inventory.getHotbar().addItemStackToSlot((short) 0, new ItemStack("Weapon_Mace_Thorium", 1)); + +// Equip armor using InventoryHelper (Hytale API utility) +InventoryHelper.useArmor(inventory.getArmor(), "Armor_Thorium_Head"); + +// Set the active hotbar slot to the weapon +inventory.setActiveHotbarSlot((byte) 0); +``` + +--- + +## Complete Example - NPC Spawn Command + +```java +import com.example.npc.NPCPlugin; +import hytale.server.plugin.npc.INonPlayerCharacter; +import hytale.server.plugin.npc.NPCEntity; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.InventoryHelper; +import com.hypixel.hytale.server.core.inventory.ItemStack; +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 org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nonnull; +import java.util.Objects; + +public class SpawnNpcCommand extends AbstractPlayerCommand { + + public SpawnNpcCommand() { + super("npc", "spawn npc"); + } + + @Override + protected void execute( + @Nonnull CommandContext commandContext, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + + // Get the player's current position + Vector3d position = playerRef.getTransform().getPosition(); + + // Define the initial rotation (facing direction) + Vector3f rotation = new Vector3f(0, 0, 0); + + // Spawn the NPC using NPCPlugin helper + Pair, INonPlayerCharacter> result = NPCPlugin.get().spawnNPC( + store, "Kweebec_Sapling", null, position, rotation); + + if (result != null) { + Ref npcRef = result.first(); + INonPlayerCharacter npc = result.second(); + + // Set up the NPC's inventory and equipment + setupNPCInventory(npcRef, store); + } + } + + /** + * Configures the inventory for the spawned NPC. + */ + public void setupNPCInventory(Ref npcRef, Store store) { + NPCEntity npcComponent = store.getComponent( + npcRef, Objects.requireNonNull(NPCEntity.getComponentType())); + + if (npcComponent == null) + return; + + // Initialize inventory size (3 rows, 9 columns, 0 offset) + npcComponent.setInventorySize(3, 9, 0); + + // Add items to the initialized inventory + addItemsToNPCInventory(npcComponent.getInventory()); + } + + /** + * Adds specific items and armor to the NPC's inventory. + */ + public void addItemsToNPCInventory(Inventory inventory) { + // Add a Thorium Mace to the first hotbar slot + inventory.getHotbar().addItemStackToSlot((short) 0, new ItemStack("Weapon_Mace_Thorium", 1)); + + // Equip a Thorium Helmet + InventoryHelper.useArmor(inventory.getArmor(), "Armor_Thorium_Head"); + + // Set the active hotbar slot to the weapon + inventory.setActiveHotbarSlot((byte) 0); + } +} +``` + +--- + +## Registering the Command + +Register the command in your main plugin class: + +```java +public class MyHytaleMod extends JavaPlugin { + @Override + protected void setup() { + this.getCommandRegistry().registerCommand(new SpawnNpcCommand()); + } +} +``` + +--- + +## Key Points + +1. **NPCPlugin simplifies spawning**: Abstracts Holder boilerplate — creates the entity, attaches the model, and positions it automatically. +2. **Always null-check the result**: `spawnNPC()` can return `null` if spawning fails. +3. **Initialize inventory before adding items**: Call `npcComponent.setInventorySize(rows, cols, offset)` before accessing the inventory. +4. **InventoryHelper for armor**: Use `InventoryHelper.useArmor()` — a Hytale API utility — to equip armor pieces. +5. **Entity key is the model/type name**: Pass the NPC type key (e.g., `"Kweebec_Sapling"`) matching assets in the server data. +6. **ECS reference for further modification**: Use `result.first()` (`Ref`) to add/remove components on the NPC after spawning. + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| `spawnNPC()` returns `null` | Verify the entity key (e.g., `"Kweebec_Sapling"`) matches a valid entity type | +| NPC has no items | Ensure `setInventorySize()` is called before adding items | +| NPC not visible | Confirm the spawn position is within a loaded chunk | +| `NPCEntity.getComponentType()` returns `null` | Verify NPCPlugin dependency is loaded and entity module is available | +| NPC facing wrong direction | Adjust the `Vector3f rotation` values | + +--- + +## Related Skills + +- `hytale-ecs` — Entity Component System patterns +- `hytale-items` — Item registry and ItemStack usage +- `hytale-commands` — Command creation patterns + +--- + +## Reference + +- Source: [Hytale Modding - Spawning NPCs Guide](https://hytalemodding.dev/en/docs/guides/plugin/spawning-npcs) diff --git a/skills/hytale-tag-system/SKILL.md b/skills/hytale-tag-system/SKILL.md new file mode 100644 index 0000000..b627243 --- /dev/null +++ b/skills/hytale-tag-system/SKILL.md @@ -0,0 +1,272 @@ +--- +name: hytale-tag-system +description: Documents Hytale's hierarchical tag system and how to use it in Hyforged. Use when implementing tag-based lookups, defining tagged assets, or understanding tag expansion patterns. Triggers - tags, tagging, AssetRegistry, tag categories, tag queries. +--- + +# Hytale Tag System + +This skill documents Hytale's hierarchical tag system and how Hyforged integrates with it for stats, items, blocks, and other assets. + +## Overview + +Hytale uses a **hierarchical tag system** where tags are defined as a map of categories to value arrays. This creates a rich, queryable tag structure that enables flexible asset lookups. + +### Key Concepts + +| Concept | Description | +|---------|-------------| +| **Tag Category** | A named group (e.g., `"Type"`, `"Element"`, `"Domain"`) | +| **Tag Value** | Values within a category (e.g., `["fire", "elemental"]`) | +| **Tag Index** | Integer index for O(1) lookups via `AssetRegistry` | +| **Tag Expansion** | Hierarchical tags expand to multiple searchable strings | + +--- + +## JSON Format + +Tags in Hytale assets use a **map structure**, not a flat array: + +```json +{ + "Tags": { + "Category1": ["value1", "value2"], + "Category2": ["value3"] + } +} +``` + +### Example: Item Tags +```json +{ + "Id": "hytale:adamantite_axe", + "Tags": { + "Type": ["Weapon"], + "Family": ["Axe"] + } +} +``` + +### Example: Stat Tags +```json +{ + "Id": "hyforged:fire-resistance-bps", + "Tags": { + "Domain": ["defense"], + "Type": ["resistance"], + "Element": ["fire", "elemental"], + "Modifier": ["percent"], + "Source": ["derived"] + } +} +``` + +--- + +## Tag Expansion + +When tags are loaded, Hytale's `AssetExtraInfo.Data.putTags()` **expands** each entry into multiple searchable tags: + +| Input | Expanded Tags | +|-------|---------------| +| `"Domain": ["offense"]` | `Domain`, `offense`, `Domain=offense` | +| `"Element": ["fire", "elemental"]` | `Element`, `fire`, `elemental`, `Element=fire`, `Element=elemental` | + +### Expansion Rules + +For each entry `"Category": ["val1", "val2", ...]`: +1. The **category key** becomes a tag: `Category` +2. Each **value** becomes a tag: `val1`, `val2` +3. Each **category=value** combination becomes a tag: `Category=val1`, `Category=val2` + +This enables flexible querying: +- `hasTag("fire")` - matches any asset with "fire" in ANY category +- `hasTag("Element=fire")` - matches only assets with `"Element": ["fire"]` +- `hasTag("Element")` - matches any asset with an Element category + +--- + +## AssetRegistry API + +Hytale's `AssetRegistry` provides the global tag index system: + +### Core Methods + +```java +// Get existing tag index (returns Integer.MIN_VALUE if not found) +int tagIndex = AssetRegistry.getTagIndex("fire"); + +// Get or create tag index (creates if not existing) +int tagIndex = AssetRegistry.getOrCreateTagIndex("fire"); +``` + +### Integer Indices + +Tags are stored as integer indices for O(1) lookups: + +```java +// Fast membership test +IntSet entityTags = entity.getData().getExpandedTagIndexes(); +int fireIndex = AssetRegistry.getTagIndex("fire"); +if (entityTags.contains(fireIndex)) { + // Entity has fire tag +} +``` + +--- + +## StatDefinitionRegistry Tag API + +The Hyforged stat system provides convenience methods for tag queries: + +### Basic Tag Methods + +```java +StatDefinitionRegistry registry = StatDefinitionRegistry.get(); + +// Check if any stat has a tag +boolean exists = registry.hasTag("fire"); + +// Get stats by tag (any expanded tag) +Collection stats = registry.getStatsForTag("fire"); +Set indices = registry.getStatIndicesForTag("fire"); +List statIds = registry.getStatIdsForTag("fire"); +``` + +### Category-Based Methods (Recommended) + +For hierarchical tags, use the explicit category-based API: + +```java +// Check if any stat has Type=resistance +boolean exists = registry.hasTagValue("Type", "resistance"); + +// Get all resistance stats +Collection resistances = registry.getStatsForTagValue("Type", "resistance"); + +// Get fire elemental stats +Set fireStats = registry.getStatIndicesForTagValue("Element", "fire"); + +// Get all ability score stat IDs +List abilityScores = registry.getStatIdsForTagValue("Type", "ability-score"); +``` + +### Integer Index Methods (Performance) + +For hot paths, use pre-resolved integer indices: + +```java +// Resolve once, use many times +int fireTagIndex = registry.getOrCreateTagIndex("Element=fire"); + +// Fast O(1) lookup +IntSet stats = registry.getStatIndicesForTagIndex(fireTagIndex); +``` + +--- + +## Standard Tag Categories + +### Stats + +| Category | Values | Purpose | +|----------|--------|---------| +| `Domain` | `offense`, `defense`, `resource`, `utility`, `attributes` | Primary functional classification | +| `Element` | `physical`, `fire`, `cold`, `lightning`, `chaos`, `elemental` | Damage/resistance element | +| `Type` | `damage`, `resistance`, `rating`, `ability-score`, `speed`, `critical`, `ailment`, `leech`, `skill-level`, `area`, `resource` | What the stat represents | +| `Modifier` | `flat`, `percent`, `more` | How the stat value applies | +| `Source` | `derived`, `base` | Origin of the stat value | +| `Mechanic` | `attack`, `spell`, `projectile`, `melee`, `ranged`, `minion`, `aura`, `totem`, `trap` | Usage mechanism | +| `Resource` | `health`, `mana`, `stamina`, `rage` | Which resource it affects | +| `Ailment` | `bleed`, `poison`, `ignite`, `chill`, `shock`, `freeze` | Specific ailment type | +| `Weapon` | `sword`, `axe`, `mace`, `dagger`, `bow`, `crossbow`, `staff`, `unarmed` | Weapon type affinity | + +### Items (Hytale Native) + +| Category | Values | Purpose | +|----------|--------|---------| +| `Type` | `Weapon`, `Armor`, `Tool`, `Consumable`, `Material` | Item classification | +| `Family` | `Sword`, `Axe`, `Helmet`, `Chestplate`, etc. | Item family | +| `Material` | `Wood`, `Stone`, `Iron`, `Gold`, `Adamantite` | Material type | +| `Tier` | `Basic`, `Common`, `Rare`, `Epic`, `Legendary` | Quality tier | + +--- + +## Implementing Tags in New Assets + +### 1. Define JSON Schema with Map Codec + +```java +// In your asset class +public static final AssetBuilderCodec CODEC = AssetBuilderCodec + .builder(MyAsset.class, MyAsset::new, ...) + .appendInherited( + new KeyedCodec<>("Tags", new MapCodec<>(Codec.STRING_ARRAY, HashMap::new)), + (asset, value) -> asset.rawTags = value != null ? value : new HashMap<>(), + asset -> asset.rawTags, + (asset, parent) -> asset.rawTags = new HashMap<>(parent.rawTags) + ) + .add() + .build(); + +private Map rawTags = new HashMap<>(); +``` + +### 2. Expand Tags on Load + +```java +public Set getExpandedTags() { + Set expanded = new HashSet<>(); + for (Map.Entry entry : rawTags.entrySet()) { + String category = entry.getKey(); + expanded.add(category); + for (String value : entry.getValue()) { + expanded.add(value); + expanded.add(category + "=" + value); + } + } + return expanded; +} +``` + +### 3. Register Tags with AssetRegistry + +```java +// During asset registration +for (String tag : asset.getExpandedTags()) { + int tagIndex = AssetRegistry.getOrCreateTagIndex(tag); + tagToAssetIndices.computeIfAbsent(tagIndex, k -> new IntOpenHashSet()).add(assetIndex); +} +``` + +--- + +## Best Practices + +### DO + +- ✅ Use category-based API (`getStatsForTagValue("Type", "resistance")`) for explicit queries +- ✅ Pre-resolve tag indices for hot paths +- ✅ Use consistent category names across asset types +- ✅ Document your tag categories in the asset schema +- ✅ Use IntSet for efficient tag membership tests + +### DON'T + +- ❌ Use flat tag arrays (`"Tags": ["fire", "damage"]`) - use hierarchical format +- ❌ Create duplicate tags across different categories with same meaning +- ❌ Store tag strings at runtime - resolve to indices +- ❌ Assume tag order matters - it doesn't + +--- + +## Related Files + +- `AssetRegistry` - Hytale's global tag index registry +- `AssetExtraInfo.Data.putTags()` - Tag expansion logic +- `StatDefinitionRegistry` - Stat-specific tag queries +- `StatDefinitionAsset` - JSON codec for stat tags +- `TagSet` / `TagSetLookupTable` - Advanced tag grouping (like NPCGroup) + +## ADR Reference + +See ADR-0008 in `.memory_bank/ADRs.md` for the decision rationale behind adopting Hytale's tag system. diff --git a/skills/hytale-teleporting-players/SKILL.md b/skills/hytale-teleporting-players/SKILL.md new file mode 100644 index 0000000..fae6934 --- /dev/null +++ b/skills/hytale-teleporting-players/SKILL.md @@ -0,0 +1,189 @@ +--- +name: hytale-teleporting-players +description: Documents how to teleport players in Hytale plugins using the Teleport component and Transform. Use when teleporting a player to a position, changing player world, setting player rotation, or building teleport utilities. Triggers - teleport, Teleport, teleport player, Transform, Rotation, createForPlayer, Teleport component, move player, warp, position, Vector3d. +--- + +# Hytale Teleporting Players + +Use this skill when teleporting players to a position or world in Hytale plugins. Teleportation works by attaching a `Teleport` component to the player entity with the desired target world and transform. You can only trigger teleports from the world where the player is currently present. + +> **Source:** + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **Teleport** | ECS component that triggers player teleportation when added to an entity | +| **Transform** | Represents a position (`Vector3d`) and rotation (`Rotation`) | +| **Rotation** | Represents yaw, pitch, and roll (roll is always `0`) | +| **createForPlayer** | Factory method on `Teleport` to create a player teleport | +| **getComponentType** | Returns the `Teleport` component type for store operations | + +--- + +## Key Concepts + +### Teleport Component + +`Teleport` is an ECS component. Adding it to a player entity triggers the teleport. It provides factory methods (`createForPlayer`) to configure the destination world and transform. + +### Transform + +A `Transform` encapsulates a position and rotation: +- **Position**: `Vector3d(x, y, z)` — world coordinates +- **Rotation**: `Rotation(yaw, pitch, 0)` — the last value (roll) should always be `0` + +### Constraints + +- You can **only trigger teleports from the world where the player is currently present**. +- Requires a `Ref` and `Store` for the player entity. + +--- + +## Required Imports + +```java +import com.hypixel.hytale.server.world.World; +import com.hypixel.hytale.server.world.Transform; +import com.hypixel.hytale.server.world.Rotation; +import com.hypixel.hytale.server.world.Teleport; + +import com.hypixel.ecs.Ref; +import com.hypixel.ecs.Store; +import com.hypixel.ecs.EntityStore; + +import org.joml.Vector3d; +``` + +--- + +## Basic Teleport (Position Only) + +Teleport a player to x/y/z coordinates in a target world: + +```java +public static void teleportPlayer(Ref ref, Store store, + World targetWorld, double x, double y, double z) { + Transform transform = new Transform(x, y, z); + Teleport teleport = Teleport.createForPlayer(targetWorld, transform); + store.addComponent(ref, Teleport.getComponentType(), teleport); +} +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ref` | `Ref` | Reference to the player entity | +| `store` | `Store` | The entity store for the player's current world | +| `targetWorld` | `World` | The world to teleport the player into | +| `x`, `y`, `z` | `double` | Target position coordinates | + +--- + +## Teleport with Rotation + +Teleport a player to a position and set their facing direction: + +```java +public static void teleportPlayer(Ref ref, Store store, + World targetWorld, double x, double y, double z, + float yaw, float pitch) { + Transform transform = new Transform( + new Vector3d(x, y, z), + new Rotation(yaw, pitch, 0) // Roll is always 0 + ); + Teleport teleport = Teleport.createForPlayer(targetWorld, transform); + store.addComponent(ref, Teleport.getComponentType(), teleport); +} +``` + +### Rotation Values + +| Field | Type | Description | +|-------|------|-------------| +| `yaw` | `float` | Horizontal rotation (left/right) | +| `pitch` | `float` | Vertical rotation (up/down) | +| `roll` | `float` | Always `0` | + +--- + +## Transform Construction + +### Position-Only Transform + +```java +Transform transform = new Transform(x, y, z); +``` + +### Position + Rotation Transform + +```java +Transform transform = new Transform( + new Vector3d(x, y, z), // Position + new Rotation(yaw, pitch, 0) // Rotation (roll always 0) +); +``` + +--- + +## Usage Notes + +- **Thread safety**: Execute teleport operations within the world's execution context (`world.execute(() -> { ... })`) if called from outside the world tick. +- **Same-world teleport**: Pass the player's current world as `targetWorld` to teleport within the same world. +- **Cross-world teleport**: Pass a different `World` object to move the player between worlds. +- **Instance teleportation**: For teleporting into instanced worlds, see the `hytale-instances` skill which provides higher-level APIs via `InstancesPlugin`. + +--- + +## Complete Example: Teleport Command Utility + +```java +import com.hypixel.ecs.EntityStore; +import com.hypixel.ecs.Ref; +import com.hypixel.ecs.Store; +import com.hypixel.hytale.server.world.Transform; +import com.hypixel.hytale.server.world.Rotation; +import com.hypixel.hytale.server.world.Teleport; +import com.hypixel.hytale.server.world.World; +import org.joml.Vector3d; + +public class TeleportUtil { + + /** + * Teleport a player to coordinates in a target world. + */ + public static void teleport(Ref ref, Store store, + World targetWorld, double x, double y, double z) { + Transform transform = new Transform(x, y, z); + Teleport teleport = Teleport.createForPlayer(targetWorld, transform); + store.addComponent(ref, Teleport.getComponentType(), teleport); + } + + /** + * Teleport a player to coordinates with a specific facing direction. + */ + public static void teleport(Ref ref, Store store, + World targetWorld, double x, double y, double z, + float yaw, float pitch) { + Transform transform = new Transform( + new Vector3d(x, y, z), + new Rotation(yaw, pitch, 0) + ); + Teleport teleport = Teleport.createForPlayer(targetWorld, transform); + store.addComponent(ref, Teleport.getComponentType(), teleport); + } + + /** + * Teleport a player to a pre-built Transform in a target world. + */ + public static void teleport(Ref ref, Store store, + World targetWorld, Transform transform) { + Teleport teleport = Teleport.createForPlayer(targetWorld, transform); + store.addComponent(ref, Teleport.getComponentType(), teleport); + } +} +``` +``` diff --git a/skills/hytale-text-holograms/SKILL.md b/skills/hytale-text-holograms/SKILL.md new file mode 100644 index 0000000..a7ebcc4 --- /dev/null +++ b/skills/hytale-text-holograms/SKILL.md @@ -0,0 +1,335 @@ +--- +name: hytale-text-holograms +description: Creates floating text holograms in Hytale plugins using invisible entities with nameplates. Use when displaying floating text, title holograms, labels above locations, NPC names, or any world-positioned text displays. Triggers - hologram, text hologram, nameplate, floating text, title hologram, Nameplate, world label, entity nameplate, hovering text. +--- + +# Hytale Text Holograms Skill + +Use this skill when creating floating text displays (holograms) in the world. Holograms are invisible entities with a `Nameplate` component that displays text above them. + +--- + +## Quick Reference + +| Concept | Description | +|---------|-------------| +| **Nameplate** | Component that displays text above an entity | +| **ProjectileComponent** | Used as an invisible entity shell for the hologram | +| **TransformComponent** | Positions the hologram in the world | +| **NetworkId** | Required for client-side synchronization | +| **UUIDComponent** | Gives the entity a unique identity | +| **Holder** | Used to stage components before spawning | + +--- + +## How It Works + +Text holograms work by spawning an invisible entity (using a `ProjectileComponent` as a shell) and attaching a `Nameplate` component to display floating text. The entity is invisible but the nameplate renders as floating text in the world. + +--- + +## Required Imports + +```java +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.math.vector.Vec3d; +import com.hypixel.hytale.math.vector.Vec4f; +import com.hypixel.hytale.server.core.entity.UUIDComponent; +import com.hypixel.hytale.server.core.entity.entities.ProjectileComponent; +import com.hypixel.hytale.server.core.entity.nameplate.Nameplate; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.tracker.NetworkId; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +``` + +--- + +## Step-by-Step Process + +### 1. Get World Reference + +All entity operations must run on the world thread. First, obtain the world where the hologram will spawn: + +```java +// From a player context +UUID playerUUID = ctx.sender().getUuid(); +PlayerRef playerRef = Universe.get().getPlayer(playerUUID); +World world = Universe.get().getWorld(playerRef.getWorldUuid()); +Transform playerTransform = playerRef.getTransform(); +``` + +### 2. Execute on World Thread + +Entity operations must run on the world thread: + +```java +world.execute(() -> { + // All hologram creation code goes here +}); +``` + +### 3. Create Entity Holder + +Create a holder to stage components before spawning: + +```java +Holder holder = EntityStore.REGISTRY.newHolder(); +``` + +### 4. Create Projectile Component (Entity Shell) + +The projectile provides a valid entity shell. It won't move or behave like a real projectile: + +```java +ProjectileComponent projectileComponent = new ProjectileComponent("Projectile"); +``` + +### 5. Add Position and Identity Components + +| Component | Purpose | +|-----------|---------| +| `ProjectileComponent` | Provides a valid entity shell | +| `TransformComponent` | Sets the entity's position and rotation | +| `UUIDComponent` | Gives the entity a unique identity | + +```java +holder.putComponent(ProjectileComponent.getComponentType(), projectileComponent); +holder.putComponent( + TransformComponent.getComponentType(), + new TransformComponent( + playerTransform.getPosition().clone(), + playerTransform.getRotation().clone() + ) +); +holder.ensureComponent(UUIDComponent.getComponentType()); +``` + +### 6. Initialize the Projectile + +Ensure the projectile entity is fully created before proceeding: + +```java +if (projectileComponent.getProjectile() == null) { + projectileComponent.initialize(); + if (projectileComponent.getProjectile() == null) { + return; // Initialization failed + } +} +``` + +### 7. Add Network and Nameplate Components + +| Component | Purpose | +|-----------|---------| +| `NetworkId` | Allows the entity to be synced to clients | +| `Nameplate` | The actual hologram text displayed | + +```java +holder.addComponent( + NetworkId.getComponentType(), + new NetworkId( + world.getEntityStore() + .getStore() + .getExternalData() + .takeNextNetworkId() + ) +); + +holder.addComponent( + Nameplate.getComponentType(), + new Nameplate("Your Hologram Text Here") +); +``` + +### 8. Spawn the Entity + +Insert the hologram entity into the world: + +```java +world.getEntityStore() + .getStore() + .addEntity(holder, com.hypixel.hytale.component.AddReason.SPAWN); +``` + +--- + +## Complete Example - Hologram Command + +```java +package org.example.plugin; + +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase; +import com.hypixel.hytale.server.core.entity.UUIDComponent; +import com.hypixel.hytale.server.core.entity.entities.ProjectileComponent; +import com.hypixel.hytale.server.core.entity.nameplate.Nameplate; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.tracker.NetworkId; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import javax.annotation.Nonnull; +import java.util.UUID; + +public class TitleHologramCommand extends CommandBase { + + public TitleHologramCommand() { + super("TitleHologram", "Create a title hologram."); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + UUID playerUUID = ctx.sender().getUuid(); + PlayerRef playerRef = Universe.get().getPlayer(playerUUID); + World world = Universe.get().getWorld(playerRef.getWorldUuid()); + Transform playerTransform = playerRef.getTransform(); + + world.execute(() -> { + Holder holder = EntityStore.REGISTRY.newHolder(); + ProjectileComponent projectileComponent = new ProjectileComponent("Projectile"); + + holder.putComponent(ProjectileComponent.getComponentType(), projectileComponent); + holder.putComponent( + TransformComponent.getComponentType(), + new TransformComponent( + playerTransform.getPosition().clone(), + playerTransform.getRotation().clone() + ) + ); + holder.ensureComponent(UUIDComponent.getComponentType()); + + if (projectileComponent.getProjectile() == null) { + projectileComponent.initialize(); + if (projectileComponent.getProjectile() == null) { + return; + } + } + + holder.addComponent( + NetworkId.getComponentType(), + new NetworkId( + world.getEntityStore() + .getStore() + .getExternalData() + .takeNextNetworkId() + ) + ); + + holder.addComponent( + Nameplate.getComponentType(), + new Nameplate("Testing Holograms") + ); + + world.getEntityStore() + .getStore() + .addEntity(holder, com.hypixel.hytale.component.AddReason.SPAWN); + }); + } +} +``` + +--- + +## Spawning at Custom Position + +To spawn a hologram at a specific position instead of the player's location: + +```java +Vec3d position = new Vec3d(100.0, 65.0, 200.0); +Vec4f rotation = new Vec4f(0, 0, 0, 1); // Identity quaternion + +holder.putComponent( + TransformComponent.getComponentType(), + new TransformComponent(position, rotation) +); +``` + +--- + +## Utility Method Example + +Create a reusable utility method for spawning holograms: + +```java +public static void spawnHologram(World world, Vec3d position, String text) { + world.execute(() -> { + Holder holder = EntityStore.REGISTRY.newHolder(); + ProjectileComponent projectileComponent = new ProjectileComponent("Projectile"); + + holder.putComponent(ProjectileComponent.getComponentType(), projectileComponent); + holder.putComponent( + TransformComponent.getComponentType(), + new TransformComponent(position.clone(), new Vec4f(0, 0, 0, 1)) + ); + holder.ensureComponent(UUIDComponent.getComponentType()); + + if (projectileComponent.getProjectile() == null) { + projectileComponent.initialize(); + if (projectileComponent.getProjectile() == null) { + return; + } + } + + holder.addComponent( + NetworkId.getComponentType(), + new NetworkId( + world.getEntityStore() + .getStore() + .getExternalData() + .takeNextNetworkId() + ) + ); + + holder.addComponent( + Nameplate.getComponentType(), + new Nameplate(text) + ); + + world.getEntityStore() + .getStore() + .addEntity(holder, com.hypixel.hytale.component.AddReason.SPAWN); + }); +} +``` + +--- + +## Key Points + +1. **World Thread Required**: All entity operations must run inside `world.execute(() -> { ... })` +2. **ProjectileComponent**: Used as an invisible shell - it won't move or behave like a projectile +3. **NetworkId Required**: Without this, the hologram won't sync to clients +4. **UUIDComponent Required**: Provides unique entity identity +5. **Initialize Projectile**: Must call `projectileComponent.initialize()` before spawning +6. **Nameplate Text**: The text passed to `new Nameplate(text)` will be displayed as floating text + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Hologram not visible | Ensure `NetworkId` component is added | +| Entity not spawning | Check that `projectileComponent.initialize()` succeeded | +| Wrong position | Verify `TransformComponent` has correct coordinates | +| Crashes on spawn | Ensure code runs inside `world.execute()` | + +--- + +## Credits + +Thanks to Al3xWarrior and Quito on Discord for initially discovering this feature, and Elie for the initial code snippet. + +--- + +## Reference + +- Source: [Hytale Modding - Text Hologram Guide](https://hytalemodding.dev/en/docs/guides/plugin/text-hologram) diff --git a/skills/hytale-ui-modding/SKILL.md b/skills/hytale-ui-modding/SKILL.md new file mode 100644 index 0000000..72b644a --- /dev/null +++ b/skills/hytale-ui-modding/SKILL.md @@ -0,0 +1,53 @@ +--- +name: hytale-ui-modding +description: Comprehensive guidance for Hytale plugin UI modding using native .ui files, Common.ui styling, layout and markup rules, and the Java UI API (CustomUIHud, MultipleHUD, CustomUIPage, InteractiveCustomUIPage). Use when creating or updating custom HUDs/pages, writing .ui markup, binding UI events, or troubleshooting UI issues. +--- + +# Hytale Native UI Modding Skill + +Use this skill for Hytale's native .ui system and the server-side Java UI API. + +Important: use native UI only. Do not use HyUI. + +## Quick start + +1. Read the architecture overview first. See references/overview.md. +2. Put .ui files under src/main/resources/Common/UI/Custom/. +3. Import Common.ui when you want shared styles and components. See references/common-styling.md. +4. Build layout with Anchor, Padding, and LayoutMode. See references/layout.md. +5. Use markup patterns like named expressions, templates, and translations. See references/markup.md. +6. Bind UI events with UIEventBuilder and always sendUpdate after handling events. See references/java-api.md and references/events.md. +7. For assets, use @2x.png and set IncludesAssetPack in manifest. See references/assets-and-packaging.md. +8. If something fails at runtime, check references/troubleshooting.md. + +## Reference library + +- references/INDEX.md +- references/overview.md +- references/common-styling.md +- references/layout.md +- references/markup.md +- references/type-documentation.md +- references/java-api.md +- references/events.md +- references/assets-and-packaging.md +- references/examples.md +- references/troubleshooting.md + +## Project conventions + +- .ui base path: Common/UI/Custom/. Relative paths inside .ui are resolved from the file location. +- Use %translation.key in .ui and add the key to the language files under src/main/resources/Server/Languages. +- Use MultipleHUD for multiple HUDs per player. Do not rely on a single CustomUIHud instance. + +## Official documentation + +- https://hytalemodding.dev/en/docs/official-documentation/custom-ui/common-styling +- https://hytalemodding.dev/en/docs/official-documentation/custom-ui/layout +- https://hytalemodding.dev/en/docs/official-documentation/custom-ui/markup +- https://hytalemodding.dev/en/docs/official-documentation/custom-ui/type-documentation + +## Notes on recent doc updates + +- The official type documentation is a generated index. Use it when you need the exact property name, type, or enum values for an element. +- Common.ui is the preferred source for cohesive styling. Import it and reference styles instead of duplicating them. diff --git a/skills/hytale-ui-modding/references/INDEX.md b/skills/hytale-ui-modding/references/INDEX.md new file mode 100644 index 0000000..7b60d02 --- /dev/null +++ b/skills/hytale-ui-modding/references/INDEX.md @@ -0,0 +1,66 @@ +# Hytale UI Modding Reference Index + +A comprehensive library of UI modding documentation for Hytale plugins. + +--- + +## Reference Files + +### Core Concepts + +| File | Description | +|------|-------------| +| [overview.md](overview.md) | **Architecture overview** - What Custom UI is, how it fits into Hytale (Client UI vs Server UI), command-based architecture diagram, key principles (Declarative, Asset-Driven, Event-Driven, Selector-Based). | +| [layout.md](layout.md) | **Layout system** - Anchor positioning/sizing, Padding, LayoutMode (vertical/horizontal stacking, centering, scrolling, wrapping), FlexWeight distribution, Visibility. Includes visual diagrams. | +| [markup.md](markup.md) | **Markup syntax** - Element declarations, named expressions (@), templates, document references ($), property types (strings, colors, objects, arrays), translation keys (%). | +| [common-styling.md](common-styling.md) | **Common.ui integration** - Import patterns, referencing styles and components, available templates (@TextButton, @Container, etc.), Value.ref() in Java. | +| [type-documentation.md](type-documentation.md) | **Type reference overview** - How to navigate the official type docs, commonly used elements/properties/enums quick reference. | +| [types.md](types.md) | **Complete types reference** - Detailed documentation of ALL UI elements (Group, Label, Button, TextField, ItemGrid, etc.), property types (Anchor, Padding, PatchStyle, LabelStyle, etc.), and enums (LayoutMode, LabelAlignment, etc.) with full property tables. | + +### Java API + +| File | Description | +|------|-------------| +| [java-api.md](java-api.md) | **Java UI classes** - CustomUIHud, MultipleHUD (MHUD for multiple HUDs), CustomUIPage, InteractiveCustomUIPage with event handling, UICommandBuilder, UIEventBuilder, threading requirements, ECS patterns. | +| [events.md](events.md) | **Event binding** - Complete CustomUIEventBindingType reference (Activating, ValueChanged, SlotClicking, etc.), value references with @ prefix, EventData codec patterns. | + +### Practical Guides + +| File | Description | +|------|-------------| +| [examples.md](examples.md) | **Complete examples** - Simple HUD, interactive dialog, search page with dynamic results, item grid inventory. Full .ui files and Java implementations. | +| [assets-and-packaging.md](assets-and-packaging.md) | **Assets & packaging** - File locations, @2x.png naming, manifest.json IncludesAssetPack, UIPath resolution, image formats. | +| [translations.md](translations.md) | **Translations & Localization** - Using translation keys (%) in UI, language files (.lang), LocalizableString in Java, translation parameters. Also covers contributing to HytaleModding website translations via Crowdin. | +| [troubleshooting.md](troubleshooting.md) | **Common issues** - UI stuck on loading, blank pages, missing images, event handling problems, selector issues, threading crashes. | + +--- + +## Quick Navigation by Task + +| I want to... | Read | +|--------------|------| +| Understand the UI architecture | [overview.md](overview.md) | +| Position an element | [layout.md](layout.md) - Anchor section | +| Stack elements vertically/horizontally | [layout.md](layout.md) - LayoutMode section | +| Create reusable UI components | [markup.md](markup.md) - Templates section | +| Use game-styled buttons | [common-styling.md](common-styling.md) - Components section | +| Look up element properties | [types.md](types.md) - Elements section | +| Look up property type fields | [types.md](types.md) - Property Types section | +| Look up enum values | [types.md](types.md) - Enums section | +| Handle button clicks | [events.md](events.md) - Activating event | +| Create a HUD | [java-api.md](java-api.md) - CustomUIHud section | +| Show multiple HUDs | [java-api.md](java-api.md) - MultipleHUD section | +| Create an interactive page | [java-api.md](java-api.md) - InteractiveCustomUIPage section | +| Display an item grid | [examples.md](examples.md) - Item Grid Inventory | +| Localize/translate UI text | [translations.md](translations.md) - Translation Keys section | +| Contribute website translations | [translations.md](translations.md) - HytaleModding Translations section | +| Fix "stuck on loading" | [troubleshooting.md](troubleshooting.md) | + +--- + +## Official Documentation Sources + +- [Common Styling](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/common-styling) +- [Layout](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/layout) +- [Markup](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/markup) +- [Type Documentation](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/type-documentation) diff --git a/skills/hytale-ui-modding/references/assets-and-packaging.md b/skills/hytale-ui-modding/references/assets-and-packaging.md new file mode 100644 index 0000000..be9ee80 --- /dev/null +++ b/skills/hytale-ui-modding/references/assets-and-packaging.md @@ -0,0 +1,30 @@ +# Assets and Packaging + +## UI assets + +- Images must use the @2x.png suffix. +- Store assets under Common/UI/Custom/. +- Reference them with TexturePath: "MyImage.png". + +Example: + +```ui +Sprite { + TexturePath: "Icons/MyIcon.png"; +} +``` + +Files on disk: + +- src/main/resources/Common/UI/Custom/Icons/MyIcon@2x.png + +## manifest.json + +Ensure IncludesAssetPack is enabled so custom UI assets are shipped to clients. + +## UIPath rules + +Paths are relative to the current .ui file: + +- "MyButton.png" resolves next to the .ui file +- "../MyButton.png" goes up one folder diff --git a/skills/hytale-ui-modding/references/common-styling.md b/skills/hytale-ui-modding/references/common-styling.md new file mode 100644 index 0000000..95501fa --- /dev/null +++ b/skills/hytale-ui-modding/references/common-styling.md @@ -0,0 +1,141 @@ +# Common Styling Reference + +This document describes the shared UI components and styles defined in `Common.ui`. Use these to create custom UIs that match the base game's visual style. + +## Overview + +The `Common.ui` file provides shared styles and components that deliver a cohesive UI experience with the core game UI. These are pre-built, battle-tested components you should prefer over creating your own from scratch. + +## Location + +Common.ui is located at `Common/UI/Custom/Common.ui` within the Hytale pack. + +--- + +## Importing Common.ui + +### Direct Import (file in Common/UI/Custom/) + +If your .ui file is directly in `Common/UI/Custom/`: + +```ui +$Common = "Common.ui"; + +// Then reference styles and components: +$Common.@TextButton { @Text = "My Button"; } +$Common.@Container { ... } +``` + +### Relative Import (file in subfolder) + +If your custom UI document is in a subfolder of `Common/UI/Custom/`, use relative path traversal: + +```ui +$Common = "../Common.ui"; +``` + +For deeper nesting: + +```ui +// Two levels deep +$Common = "../../Common.ui"; +``` + +See the [Markup path documentation](markup.md) for more details on path resolution. + +--- + +## Referencing Styles + +Once imported, reference styles from Common.ui using the `$Common.@StyleName` syntax: + +```ui +$Common = "../Common.ui"; + +Label { + Style: $Common.@DefaultLabelStyle; +} + +Group { + ScrollbarStyle: $Common.@DefaultScrollbar; +} +``` + +--- + +## Referencing Components (Templates) + +Common.ui also provides pre-built component templates: + +```ui +$Common = "../Common.ui"; + +Group #ButtonRow { + LayoutMode: Left; + + // Use the TextButton template + $Common.@TextButton #SaveButton { + @Text = "Save"; + } + + $Common.@TextButton #CancelButton { + @Text = "Cancel"; + } +} +``` + +--- + +## Common Components and Styles + +The exact list of available styles and components can be viewed via the `/ui-gallery` command in-game. (Note: This command is planned for a future patch.) + +### Frequently Used Styles + +| Style Name | Description | +|------------|-------------| +| `@DefaultLabelStyle` | Standard label text styling | +| `@DefaultButtonStyle` | Standard button styling | +| `@DefaultScrollbar` | Default scrollbar for scrolling groups | + +### Frequently Used Components + +| Component | Description | +|-----------|-------------| +| `@TextButton` | Primary styled button | +| `@SecondaryTextButton` | Secondary styled button | +| `@TertiaryTextButton` | Tertiary styled button | +| `@CancelTextButton` | Cancel/destructive button | +| `@BackButton` | Back navigation button | +| `@Container` | Styled window frame with title | +| `@PageOverlay` | Full-screen overlay background | +| `@NumberField` | Numeric input field | +| `@AssetImage` | Asset image display | +| `@CheckBoxWithLabel` | Checkbox with text label | + +--- + +## Best Practices + +1. **Always import Common.ui** - Use shared styles instead of duplicating them locally +2. **Use relative paths correctly** - Adjust the path based on your file's location relative to `Common/UI/Custom/` +3. **Prefer templates over raw elements** - Use `$Common.@TextButton` instead of building a button from scratch +4. **Check /ui-gallery** - When available, use this command to see live examples of all Common.ui styles + +--- + +## Value References from Java + +In Java code, you can reference Common.ui styles: + +```java +import com.hypixel.hytale.server.core.ui.Value; + +// Reference a style from Common.ui +commands.set("#Element.Style", Value.ref("Common.ui", "DefaultButtonStyle")); +commands.set("#ScrollGroup.ScrollbarStyle", Value.ref("Common.ui", "DefaultScrollbar")); +``` + +--- + +Source: https://hytalemodding.dev/en/docs/official-documentation/custom-ui/common-styling diff --git a/skills/hytale-ui-modding/references/events.md b/skills/hytale-ui-modding/references/events.md new file mode 100644 index 0000000..8d2876c --- /dev/null +++ b/skills/hytale-ui-modding/references/events.md @@ -0,0 +1,216 @@ +# UI Event Binding Types + +Use UIEventBuilder.addEventBinding to bind events to elements in your UI. + +## Event Binding Syntax + +```java +events.addEventBinding( + CustomUIEventBindingType.EventType, // The event type + "#ElementSelector", // Element to bind to + eventData, // Data to send when triggered + locksInterface // Whether to lock UI during processing +); +``` + +--- + +## Event Types Reference + +### Interaction Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `Activating` | Element is activated (button click, enter key) | Buttons, clickable elements | +| `RightClicking` | Right mouse button click | Context menus | +| `DoubleClicking` | Double click | Quick actions | +| `MouseEntered` | Mouse cursor enters element bounds | Hover effects, tooltips | +| `MouseExited` | Mouse cursor leaves element bounds | Remove hover effects | +| `MouseButtonReleased` | Mouse button released over element | Drag completion | + +### Input Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `ValueChanged` | Input value changes | TextField, Slider, CheckBox, DropdownBox | +| `FocusGained` | Element receives input focus | Input highlighting | +| `FocusLost` | Element loses input focus | Input validation, save on blur | +| `KeyDown` | Key pressed while element has focus | Keyboard shortcuts, special keys | +| `Validating` | Input validation requested | Form validation | + +### Page Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `Dismissing` | Page dismiss attempt (ESC key, close button) | Confirm dialogs, save prompts | + +### Tab Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `SelectedTabChanged` | Tab selection changes | Tab content switching | + +### Item Grid Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `SlotClicking` | ItemGrid slot clicked | Item selection, item actions | +| `SlotDoubleClicking` | ItemGrid slot double-clicked | Quick equip, quick transfer | +| `SlotMouseEntered` | Mouse enters slot | Slot hover, tooltip display | +| `SlotMouseExited` | Mouse leaves slot | Remove tooltips | + +### Drag and Drop Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `DragCancelled` | Drag operation cancelled | Reset drag state | +| `Dropped` | Item dropped on element | Item transfer, placement | +| `SlotMouseDragCompleted` | Drag completed over a slot | Item move between slots | +| `SlotMouseDragExited` | Drag exited a slot | Visual feedback | +| `SlotClickReleaseWhileDragging` | Click released while dragging | Split stacks, drop items | +| `SlotClickPressWhileDragging` | Click pressed while dragging | Multi-select | + +### Layout Events + +| Event | Trigger | Common Use | +|-------|---------|------------| +| `ElementReordered` | Element order changed in ReorderableList | List sorting | + +--- + +## Usage Patterns + +### Button Click + +```java +events.addEventBinding( + CustomUIEventBindingType.Activating, + "#SaveButton", + EventData.of("Action", "save"), + false +); +``` + +### Text Input Change + +Use the `@` prefix in the EventData key to pull the value from the UI element: + +```java +events.addEventBinding( + CustomUIEventBindingType.ValueChanged, + "#SearchField", + EventData.of("@Query", "#SearchField.Value"), + false +); +``` + +In your EventData class, the key `@Query` will receive the current value of `#SearchField.Value`. + +### Slider Value Change + +```java +events.addEventBinding( + CustomUIEventBindingType.ValueChanged, + "#VolumeSlider", + EventData.of("@Volume", "#VolumeSlider.Value"), + false +); +``` + +### Item Grid Slot Click + +```java +events.addEventBinding( + CustomUIEventBindingType.SlotClicking, + "#InventoryGrid", + EventData.of("@SlotIndex", "#InventoryGrid.SelectedSlotIndex"), + false +); +``` + +### Tab Selection + +```java +events.addEventBinding( + CustomUIEventBindingType.SelectedTabChanged, + "#TabNav", + EventData.of("@Tab", "#TabNav.SelectedTab"), + false +); +``` + +### Page Dismiss Confirmation + +```java +events.addEventBinding( + CustomUIEventBindingType.Dismissing, + "#Root", // Or the page root element + EventData.of("Action", "dismiss"), + false +); +``` + +In handleDataEvent, you can prevent dismissal by not closing the page and showing a confirmation dialog instead. + +--- + +## Value References + +The `@` prefix in EventData keys indicates that the value should be pulled from a UI element property: + +```java +EventData.of("@Key", "#ElementId.Property") +``` + +| Syntax | Meaning | +|--------|---------| +| `"Action"` | Static key, value comes from Java code | +| `"@Value"` | Dynamic key, value comes from UI element specified in the second parameter | + +### Common Value References + +| Reference | Source | +|-----------|--------| +| `#TextField.Value` | Text field current value | +| `#Slider.Value` | Slider current value | +| `#CheckBox.Value` | Checkbox checked state | +| `#DropdownBox.Value` | Selected dropdown value | +| `#ItemGrid.SelectedSlotIndex` | Selected slot index | + +--- + +## Event Data Class Pattern + +```java +public static class EventData { + public static final BuilderCodec CODEC = BuilderCodec.builder(EventData.class, EventData::new) + // Static fields + .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) + .add() + // Dynamic fields (@ prefix means value from UI) + .append(new KeyedCodec<>("@Query", Codec.STRING), (e, s) -> e.query = s, e -> e.query) + .add() + .append(new KeyedCodec<>("@SlotIndex", Codec.INT), (e, i) -> e.slotIndex = i, e -> e.slotIndex) + .add() + .build(); + + String action; + String query; + int slotIndex; + + public static EventData of(String key, String value) { + EventData data = new EventData(); + // Map keys to fields... + return data; + } +} +``` + +--- + +## Important Notes + +1. **Always call sendUpdate after handling events** in InteractiveCustomUIPage +2. **Value references (@) pull current values** from the UI at the time the event fires +3. **locksInterface parameter**: Set to `true` if you need to prevent user interaction during processing +4. **Selector must match element ID** exactly (with # prefix) diff --git a/skills/hytale-ui-modding/references/examples.md b/skills/hytale-ui-modding/references/examples.md new file mode 100644 index 0000000..4599565 --- /dev/null +++ b/skills/hytale-ui-modding/references/examples.md @@ -0,0 +1,566 @@ +# Complete UI Examples + +This document provides full, working examples of common UI patterns. + +--- + +## Example 1: Simple HUD + +A basic HUD that displays a status message. + +### Hud/SimpleHud.ui + +```ui +$Common = "../Common.ui"; + +Group #Root { + Anchor: (Bottom: 100, Left: 20, Width: 200, Height: 40); + Background: PatchStyle(Color: #1a1a2eD0); + Padding: (Full: 8); + LayoutMode: CenterMiddle; + + Label #StatusText { + Text: "Status: Ready"; + Style: (FontSize: 14, TextColor: #ffffff); + } +} +``` + +### Java Implementation + +```java +public class SimpleHud extends CustomUIHud { + private String statusText = "Status: Ready"; + + public SimpleHud(PlayerRef playerRef) { + super(playerRef); + } + + @Override + protected void build(UICommandBuilder cmd) { + cmd.append("Hud/SimpleHud.ui"); + } + + public void setStatus(String text) { + this.statusText = text; + UICommandBuilder cmd = new UICommandBuilder(); + cmd.set("#StatusText.Text", text); + sendUpdate(cmd); + } +} +``` + +--- + +## Example 2: Interactive Dialog Page + +A confirmation dialog with OK/Cancel buttons. + +### Pages/ConfirmDialog.ui + +```ui +$Common = "../Common.ui"; + +Group #Root { + Anchor: (Full: 0); + Background: PatchStyle(Color: #000000(0.6)); + LayoutMode: CenterMiddle; + + Group #Dialog { + Anchor: (Width: 400, Height: 200); + Background: PatchStyle(Color: #1a1a2eF0, Border: 4); + LayoutMode: Top; + Padding: (Full: 20); + + Label #Title { + Text: %ui.confirm.title; + Style: (FontSize: 20, RenderBold: true, HorizontalAlignment: Center); + Anchor: (Height: 32); + } + + Label #Message { + Text: %ui.confirm.message; + Style: (FontSize: 14, HorizontalAlignment: Center, Wrap: true); + Anchor: (Height: 60); + } + + Group #Spacer { + FlexWeight: 1; + } + + Group #ButtonRow { + LayoutMode: CenterMiddle; + Anchor: (Height: 50); + + Button #CancelButton { + Text: %ui.general.cancel; + Anchor: (Width: 120, Height: 36, Right: 10); + Style: $Common.@SecondaryButtonStyle; + } + + Button #OkButton { + Text: %ui.general.ok; + Anchor: (Width: 120, Height: 36); + Style: $Common.@PrimaryButtonStyle; + } + } + } +} +``` + +### Java Implementation + +```java +public class ConfirmDialog extends InteractiveCustomUIPage { + + private final String titleKey; + private final String messageKey; + private final Runnable onConfirm; + + public ConfirmDialog(PlayerRef playerRef, String titleKey, String messageKey, Runnable onConfirm) { + super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC); + this.titleKey = titleKey; + this.messageKey = messageKey; + this.onConfirm = onConfirm; + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, + UIEventBuilder events, Store store) { + cmd.append("Pages/ConfirmDialog.ui"); + + // Set dynamic text if needed + if (titleKey != null) { + cmd.set("#Title.Text", "%" + titleKey); + } + if (messageKey != null) { + cmd.set("#Message.Text", "%" + messageKey); + } + + // Bind button events + events.addEventBinding(CustomUIEventBindingType.Activating, "#OkButton", + EventData.action("confirm"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#CancelButton", + EventData.action("cancel"), false); + } + + @Override + public void handleDataEvent(Ref ref, Store store, EventData data) { + if ("confirm".equals(data.action)) { + if (onConfirm != null) { + onConfirm.run(); + } + close(ref, store); + return; + } + if ("cancel".equals(data.action)) { + close(ref, store); + return; + } + sendUpdate(null, false); + } + + private void close(Ref ref, Store store) { + Player player = store.getComponent(ref, Player.getComponentType()); + player.getPageManager().setPage(ref, store, Page.None); + } + + public static class EventData { + public static final BuilderCodec CODEC = BuilderCodec.builder(EventData.class, EventData::new) + .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) + .add() + .build(); + + String action; + + public static EventData action(String action) { + EventData data = new EventData(); + data.action = action; + return data; + } + } +} +``` + +--- + +## Example 3: Search Page with Dynamic Results + +A page with a search field that updates results dynamically. + +### Pages/SearchPage.ui + +```ui +$Common = "../Common.ui"; + +@ResultItem = Group { + Anchor: (Height: 40); + LayoutMode: Left; + Padding: (Horizontal: 10, Vertical: 5); + Background: PatchStyle(Color: #2a2a3e); + + Label #Name { + Text: @ItemName; + Style: (FontSize: 14); + Anchor: (Width: 200); + } + + Label #Description { + Text: @ItemDescription; + Style: (FontSize: 12, TextColor: #aaaaaa); + FlexWeight: 1; + } + + Button #SelectButton { + Text: "Select"; + Anchor: (Width: 80, Height: 30); + } +}; + +Group #Root { + Anchor: (Full: 0); + Background: PatchStyle(Color: #000000(0.7)); + LayoutMode: CenterMiddle; + + Group #Container { + Anchor: (Width: 600, Height: 500); + Background: PatchStyle(Color: #1a1a2eF0, Border: 4); + LayoutMode: Top; + Padding: (Full: 16); + + Label #Title { + Text: %ui.search.title; + Style: (FontSize: 22, RenderBold: true); + Anchor: (Height: 36); + } + + Group #SearchRow { + LayoutMode: Left; + Anchor: (Height: 40, Bottom: 10); + + TextField #SearchInput { + PlaceholderText: %ui.search.placeholder; + Anchor: (Height: 36); + FlexWeight: 1; + } + + Button #ClearButton { + Text: "X"; + Anchor: (Width: 36, Height: 36, Left: 8); + } + } + + Group #ResultsContainer { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $Common.@DefaultScrollbar; + + // Results will be dynamically added here + } + + Group #Footer { + LayoutMode: Right; + Anchor: (Height: 50, Top: 10); + + Label #ResultCount { + Text: "0 results"; + Style: (FontSize: 12, TextColor: #888888); + Anchor: (Width: 100); + } + + Group #Spacer { FlexWeight: 1; } + + Button #CloseButton { + Text: %ui.general.close; + Anchor: (Width: 100, Height: 36); + } + } + } +} +``` + +### Java Implementation + +```java +public class SearchPage extends InteractiveCustomUIPage { + + private final List allResults; + private List filteredResults; + + public SearchPage(PlayerRef playerRef, List results) { + super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC); + this.allResults = results; + this.filteredResults = new ArrayList<>(results); + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, + UIEventBuilder events, Store store) { + cmd.append("Pages/SearchPage.ui"); + + // Build initial results + buildResults(cmd, events); + + // Bind events + events.addEventBinding(CustomUIEventBindingType.ValueChanged, "#SearchInput", + EventData.of("@Query", "#SearchInput.Value"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#ClearButton", + EventData.of("Action", "clear"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#CloseButton", + EventData.of("Action", "close"), false); + } + + @Override + public void handleDataEvent(Ref ref, Store store, EventData data) { + if ("close".equals(data.action)) { + close(ref, store); + return; + } + + if ("clear".equals(data.action)) { + UICommandBuilder cmd = new UICommandBuilder(); + cmd.set("#SearchInput.Value", ""); + filteredResults = new ArrayList<>(allResults); + rebuildResults(cmd); + sendUpdate(cmd, false); + return; + } + + if ("select".equals(data.action) && data.itemId != null) { + handleSelection(data.itemId); + close(ref, store); + return; + } + + if (data.query != null) { + filterResults(data.query); + UICommandBuilder cmd = new UICommandBuilder(); + rebuildResults(cmd); + sendUpdate(cmd, false); + return; + } + + sendUpdate(null, false); + } + + private void filterResults(String query) { + if (query == null || query.isEmpty()) { + filteredResults = new ArrayList<>(allResults); + } else { + String lowerQuery = query.toLowerCase(); + filteredResults = allResults.stream() + .filter(r -> r.name().toLowerCase().contains(lowerQuery)) + .collect(Collectors.toList()); + } + } + + private void buildResults(UICommandBuilder cmd, UIEventBuilder events) { + for (int i = 0; i < filteredResults.size(); i++) { + SearchResult result = filteredResults.get(i); + String id = "Result" + i; + + cmd.appendInline("#ResultsContainer", String.format( + "@ResultItem #%s { @ItemName = \"%s\"; @ItemDescription = \"%s\"; }", + id, escapeString(result.name()), escapeString(result.description()) + )); + + events.addEventBinding(CustomUIEventBindingType.Activating, + "#" + id + " #SelectButton", + EventData.select(result.id()), false); + } + + cmd.set("#ResultCount.Text", filteredResults.size() + " results"); + } + + private void rebuildResults(UICommandBuilder cmd) { + cmd.clear("#ResultsContainer"); + UIEventBuilder events = new UIEventBuilder(); + buildResults(cmd, events); + // Note: In a full implementation, you'd need to send events too + } + + private String escapeString(String s) { + return s.replace("\\", "\\\\").replace("\"", "\\\""); + } + + private void handleSelection(String itemId) { + // Handle the selection + } + + private void close(Ref ref, Store store) { + Player player = store.getComponent(ref, Player.getComponentType()); + player.getPageManager().setPage(ref, store, Page.None); + } + + public static class EventData { + public static final BuilderCodec CODEC = BuilderCodec.builder(EventData.class, EventData::new) + .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) + .add() + .append(new KeyedCodec<>("@Query", Codec.STRING), (e, s) -> e.query = s, e -> e.query) + .add() + .append(new KeyedCodec<>("ItemId", Codec.STRING), (e, s) -> e.itemId = s, e -> e.itemId) + .add() + .build(); + + String action; + String query; + String itemId; + + public static EventData of(String key, String value) { + EventData data = new EventData(); + if ("Action".equals(key)) data.action = value; + return data; + } + + public static EventData select(String itemId) { + EventData data = new EventData(); + data.action = "select"; + data.itemId = itemId; + return data; + } + } + + public record SearchResult(String id, String name, String description) {} +} +``` + +--- + +## Example 4: Item Grid Inventory + +An inventory page with an item grid. + +### Pages/Inventory.ui + +```ui +$Common = "../Common.ui"; + +Group #Root { + Anchor: (Full: 0); + Background: PatchStyle(Color: #000000(0.6)); + LayoutMode: CenterMiddle; + + Group #Container { + Anchor: (Width: 450, Height: 400); + Background: PatchStyle(Color: #1a1a2eF0, Border: 4); + LayoutMode: Top; + Padding: (Full: 16); + + Label #Title { + Text: %ui.inventory.title; + Style: (FontSize: 20, RenderBold: true); + Anchor: (Height: 32, Bottom: 12); + } + + ItemGrid #InventoryGrid { + FlexWeight: 1; + SlotsPerRow: 8; + AreItemsDraggable: false; + ShowScrollbar: true; + KeepScrollPosition: true; + RenderItemQualityBackground: true; + Style: $Common.@DefaultItemGridStyle; + } + + Group #Footer { + LayoutMode: Right; + Anchor: (Height: 50, Top: 12); + + Button #CloseButton { + Text: %ui.general.close; + Anchor: (Width: 100, Height: 36); + } + } + } +} +``` + +### Java Implementation + +```java +public class InventoryPage extends InteractiveCustomUIPage { + + private final List items; + + public InventoryPage(PlayerRef playerRef, List items) { + super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC); + this.items = items; + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, + UIEventBuilder events, Store store) { + cmd.append("Pages/Inventory.ui"); + + // Build item grid slots + ItemGridSlot[] slots = items.stream() + .map(item -> new ItemGridSlot() + .setItemStack(item) + .setActivatable(true)) + .toArray(ItemGridSlot[]::new); + + cmd.setObject("#InventoryGrid.Slots", slots); + + // Bind events + events.addEventBinding(CustomUIEventBindingType.SlotClicking, "#InventoryGrid", + EventData.of("@SlotIndex", "#InventoryGrid.SelectedSlotIndex"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#CloseButton", + EventData.of("Action", "close"), false); + } + + @Override + public void handleDataEvent(Ref ref, Store store, EventData data) { + if ("close".equals(data.action)) { + close(ref, store); + return; + } + + if (data.slotIndex >= 0 && data.slotIndex < items.size()) { + ItemStack selectedItem = items.get(data.slotIndex); + handleItemClick(selectedItem, data.slotIndex); + } + + sendUpdate(null, false); + } + + private void handleItemClick(ItemStack item, int slotIndex) { + // Handle item selection + } + + private void close(Ref ref, Store store) { + Player player = store.getComponent(ref, Player.getComponentType()); + player.getPageManager().setPage(ref, store, Page.None); + } + + public static class EventData { + public static final BuilderCodec CODEC = BuilderCodec.builder(EventData.class, EventData::new) + .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) + .add() + .append(new KeyedCodec<>("@SlotIndex", Codec.INT), (e, i) -> e.slotIndex = i, e -> e.slotIndex) + .add() + .build(); + + String action; + int slotIndex = -1; + + public static EventData of(String key, String value) { + EventData data = new EventData(); + if ("Action".equals(key)) data.action = value; + return data; + } + } +} +``` + +--- + +## Key Patterns Summary + +1. **Always import Common.ui** for consistent styling +2. **Use templates (@Name)** for reusable UI components +3. **Use translation keys (%key)** for all user-facing text +4. **Use @ prefix in EventData** for dynamic value references +5. **Always call sendUpdate()** after handling events +6. **Escape strings** when building inline UI dynamically +7. **Use namespaced IDs** for HUDs with MultipleHUD +8. **Run UI operations on world thread** diff --git a/skills/hytale-ui-modding/references/java-api.md b/skills/hytale-ui-modding/references/java-api.md new file mode 100644 index 0000000..1c9bbf7 --- /dev/null +++ b/skills/hytale-ui-modding/references/java-api.md @@ -0,0 +1,527 @@ +# Java UI API Reference + +This project uses the native Hytale UI Java API. This reference covers the core classes and patterns for building custom UIs. + +> **Important**: This project uses native Hytale UI only. Do not use HyUI library. + +--- + +## Class Overview + +| Class | Purpose | +|-------|---------| +| `CustomUIHud` | Persistent overlay elements (always visible) | +| `MultipleHUD` | Library enabling multiple simultaneous HUDs per player | +| `CustomUIPage` | Static full-screen modal pages | +| `InteractiveCustomUIPage` | Interactive pages with event handling | +| `UICommandBuilder` | Java API for building/modifying UI | +| `UIEventBuilder` | Java API for binding events | + +--- + +## CustomUIHud + +CustomUIHud is used for persistent overlay elements that remain visible while the player plays. + +**Important:** Hytale only supports one CustomUIHud per player by default. Use MultipleHUD when you need more than one HUD. + +### Basic HUD Implementation + +```java +import com.hypixel.hytale.server.core.entity.entities.player.hud.CustomUIHud; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; + +public class MyHud extends CustomUIHud { + + public MyHud(PlayerRef playerRef) { + super(playerRef); + } + + @Override + protected void build(UICommandBuilder commandBuilder) { + // Append a .ui file + commandBuilder.append("Hud/MyHud.ui"); + + // Or use inline UI: + commandBuilder.appendInline(null, "Label #Status { Text: \"Hello\"; }"); + } +} +``` + +### Showing a HUD + +```java +MyHud hud = new MyHud(playerRef); +hud.show(); +``` + +### Updating a HUD + +```java +// After building, you can send updates +UICommandBuilder commands = new UICommandBuilder(); +commands.set("#Status.Text", "Updated text"); +hud.sendUpdate(commands); +``` + +--- + +## MultipleHUD (MHUD) + +By default, Hytale only allows **one** CustomUIHud per player. The MultipleHUD library (by Buuz135) provides a wrapper that allows multiple HUD elements simultaneously. + +**Dependency:** Already included in project via CurseForge Maven (`com.buuz135:MultipleHUD:1.0.2`) + +### Basic Usage + +```java +import com.buuz135.mhud.MultipleHUD; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.hud.CustomUIHud; +import com.hypixel.hytale.server.core.universe.PlayerRef; + +// Add/replace a HUD with a unique identifier +MultipleHUD.getInstance().setCustomHud(player, playerRef, "MyHudId", new MyCustomHud(playerRef)); + +// Add multiple HUDs +MultipleHUD.getInstance().setCustomHud(player, playerRef, "HealthBar", new HealthBarHud(playerRef)); +MultipleHUD.getInstance().setCustomHud(player, playerRef, "Buffs", new BuffDisplayHud(playerRef)); +MultipleHUD.getInstance().setCustomHud(player, playerRef, "Minimap", new MinimapHud(playerRef)); + +// Remove a specific HUD by identifier +MultipleHUD.getInstance().hideCustomHud(player, playerRef, "MyHudId"); + +// Replace a HUD (same identifier replaces existing) +MultipleHUD.getInstance().setCustomHud(player, playerRef, "HealthBar", new NewHealthBarHud(playerRef)); +``` + +### MHUD API Reference + +| Method | Description | +|--------|-------------| +| `MultipleHUD.getInstance()` | Get the singleton instance | +| `setCustomHud(player, playerRef, id, hud)` | Add or replace a HUD by identifier | +| `hideCustomHud(player, playerRef, id)` | Remove a HUD by identifier | + +### How MultipleHUD Works + +MHUD creates a wrapper `MultipleCustomUIHud` that contains a root group `#MultipleHUD`. Each individual HUD is added as a child group with ID `#`. The library automatically: + +- Converts HUD identifiers to valid element IDs (strips non-alphanumeric chars) +- Prefixes all selectors in your HUD with the container path +- Handles build/update lifecycle for each HUD independently + +### Empty HUD Placeholder + +Use `EmptyHUD` as a placeholder when you need a HUD slot but no content: + +```java +import com.buuz135.mhud.EmptyHUD; + +// Create an empty placeholder +MultipleHUD.getInstance().setCustomHud(player, playerRef, "Placeholder", new EmptyHUD(playerRef)); +``` + +### Recommended ECS Pattern for HUD Systems + +When creating HUD systems for Hyforged, follow this pattern (used by `CurrencyHudSystem`, `ResourceStatsHudSystem`, `CombatLogHudSystem`): + +```java +public class MyHudSystem extends DelayedEntitySystem { + + /** Check for MHUD availability at class load */ + private static final boolean MULTIPLE_HUD_AVAILABLE; + static { + boolean available = false; + try { + Class.forName("com.buuz135.mhud.MultipleHUD"); + available = true; + } catch (ClassNotFoundException e) { + LOGGER.warning("MultipleHUD not available - HUD disabled"); + } + MULTIPLE_HUD_AVAILABLE = available; + } + + /** Unique namespaced ID for this HUD */ + public static final String HUD_ID = "hyforged:my_hud"; + + /** Track HUD instances per player */ + private static final Map playerHuds = new ConcurrentHashMap<>(); + + @Override + public void tick(...) { + if (!MULTIPLE_HUD_AVAILABLE) return; + + UUID playerUuid = uuidComponent.getUuid(); + boolean shouldShowHud = /* your logic */; + + com.buuz135.mhud.MultipleHUD multipleHUD = com.buuz135.mhud.MultipleHUD.getInstance(); + MyHud existingHud = playerHuds.get(playerUuid); + + if (!shouldShowHud) { + if (existingHud != null) { + multipleHUD.hideCustomHud(player, playerRef, HUD_ID); + playerHuds.remove(playerUuid); + } + return; + } + + // Create HUD if not exists + if (existingHud == null) { + MyHud hud = new MyHud(playerRef); + multipleHUD.setCustomHud(player, playerRef, HUD_ID, hud); + playerHuds.put(playerUuid, hud); + existingHud = hud; + } + + // Update HUD with new values + existingHud.updateValues(...); + } +} +``` + +**Key Points:** +- Use `DelayedEntitySystem` to avoid updating every tick +- Check `MULTIPLE_HUD_AVAILABLE` before any MHUD calls +- Use namespaced HUD IDs like `"hyforged:my_hud"` +- Track HUD instances per player UUID +- Hide HUD before removing from tracking map +- Only create new HUD if one doesn't exist for the player + +--- + +## CustomUIPage + +CustomUIPage is used for static full-screen modal pages. + +### Basic Page Implementation + +```java +import com.hypixel.hytale.server.core.entity.entities.player.pages.CustomUIPage; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; + +public class MyPage extends CustomUIPage { + + public MyPage(PlayerRef playerRef) { + super(playerRef, CustomPageLifetime.CanDismiss); + } + + @Override + public void build(Ref ref, UICommandBuilder commandBuilder, + UIEventBuilder eventBuilder, Store store) { + commandBuilder.append("Pages/MyPage.ui"); + } +} +``` + +### Opening a Page + +```java +Player player = store.getComponent(ref, Player.getComponentType()); +player.getPageManager().setPage(ref, store, new MyPage(playerRef)); +``` + +### Page Lifetime Options + +```java +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; + +CustomPageLifetime.CanDismiss // Player can close with ESC +CustomPageLifetime.Dismiss // Closes immediately (not typically used) +``` + +--- + +## InteractiveCustomUIPage + +InteractiveCustomUIPage is used for pages with event handling. It uses a generic type parameter for the event data class. + +### Complete Interactive Page Implementation + +```java +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.Codec; + +public class MyInteractivePage extends InteractiveCustomUIPage { + + public MyInteractivePage(PlayerRef playerRef) { + super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC); + } + + @Override + public void build(Ref ref, UICommandBuilder commandBuilder, + UIEventBuilder eventBuilder, Store store) { + commandBuilder.append("Pages/MyPage.ui"); + + // Bind button click event + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#MyButton", + EventData.of("Action", "buttonClicked"), + false // locksInterface - if true, locks UI during processing + ); + + // Bind text input value change + eventBuilder.addEventBinding( + CustomUIEventBindingType.ValueChanged, + "#SearchInput", + EventData.of("@SearchValue", "#SearchInput.Value"), // @ prefix = UI value reference + false + ); + } + + @Override + public void handleDataEvent(Ref ref, Store store, EventData data) { + if ("buttonClicked".equals(data.action)) { + // Handle button click + } + if (data.searchValue != null) { + // Handle search input change + updateSearchResults(data.searchValue); + } + // IMPORTANT: Always call sendUpdate after handling events + sendUpdate(null, false); + } + + private void updateSearchResults(String query) { + UICommandBuilder commands = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + commands.clear("#Results"); + // Build updated content... + sendUpdate(commands, events, false); + } + + // Event data class with codec + public static class EventData { + public static final BuilderCodec CODEC = BuilderCodec.builder(EventData.class, EventData::new) + .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) + .add() + .append(new KeyedCodec<>("@SearchValue", Codec.STRING), (e, s) -> e.searchValue = s, e -> e.searchValue) + .add() + .build(); + + String action; + String searchValue; + + public static EventData of(String key, String value) { + EventData data = new EventData(); + if (key.equals("Action")) data.action = value; + else if (key.equals("@SearchValue")) data.searchValue = value; + return data; + } + } +} +``` + +### Critical: Always Call sendUpdate + +After handling events in `handleDataEvent`, you **must** call `sendUpdate`: + +```java +@Override +public void handleDataEvent(Ref ref, Store store, EventData data) { + // Handle your logic here... + + // ALWAYS call sendUpdate at the end + sendUpdate(null, false); // null for no UI changes, false for not closing +} +``` + +If you forget `sendUpdate`, the page will get stuck on "Loading...". + +### Closing a Page + +```java +private void close(Ref ref, Store store) { + Player player = store.getComponent(ref, Player.getComponentType()); + player.getPageManager().setPage(ref, store, Page.None); +} +``` + +--- + +## UICommandBuilder + +UICommandBuilder is used to construct UI modifications. + +### Methods + +```java +UICommandBuilder commands = new UICommandBuilder(); + +// Append .ui file content +commands.append("Pages/MyPage.ui"); // Append to root +commands.append("#Container", "Pages/Item.ui"); // Append to selector + +// Append inline UI content +// IMPORTANT: Text values MUST be quoted in inline .ui syntax +commands.appendInline("#List", "Label { Text: \"Item\"; }"); + +// Insert before element +commands.insertBefore("#Target", "Pages/Header.ui"); +commands.insertBeforeInline("#Target", "Label { Text: \"Before\"; }"); + +// Set properties +commands.set("#Label.Text", "Hello World"); +commands.set("#Label.Visible", true); +commands.set("#Slider.Value", 50); +commands.set("#Progress.Value", 0.75f); + +// Set complex objects +commands.setObject("#Element.Anchor", new Anchor().setWidth(Value.of(200))); +commands.setObject("#Grid.Slots", new ItemGridSlot[]{ new ItemGridSlot(itemStack) }); + +// Set with value reference (reference styles from Common.ui) +commands.set("#Button.Style", Value.ref("Common.ui", "DefaultButtonStyle")); + +// Remove/clear +commands.remove("#Element"); // Remove element +commands.clear("#Container"); // Clear children +commands.setNull("#Label.Text"); // Set to null +``` + +### Selector Syntax + +Selectors target elements by their ID and optionally their properties: + +| Selector | Meaning | +|----------|----------| +| `#ElementId` | Target element by ID | +| `#ElementId.Property` | Target element's property | +| `#Parent #Child` | Nested element selection | +| `#List[0]` | First child of element "List" (indexed access) | +| `#List[0] #Title` | Element "Title" within the first child of "List" | + +--- + +## UIEventBuilder + +UIEventBuilder is used to bind UI events to handler methods. + +### Basic Event Binding + +```java +UIEventBuilder events = new UIEventBuilder(); + +// Basic event binding (no data) +events.addEventBinding(CustomUIEventBindingType.Activating, "#Button"); + +// With data payload +events.addEventBinding( + CustomUIEventBindingType.Activating, + "#Button", + EventData.of("Action", "save"), + false // locksInterface +); + +// Value reference (gets value from UI element) +events.addEventBinding( + CustomUIEventBindingType.ValueChanged, + "#TextField", + EventData.of("@Value", "#TextField.Value"), // @ prefix = UI value reference + false +); +``` + +### Event Binding Parameters + +| Parameter | Description | +|-----------|-------------| +| Event type | Type of event to listen for (see events.md) | +| Selector | Element ID to attach the event to | +| Data | Event data to send when triggered | +| locksInterface | If true, locks the UI during event processing | + +See [events.md](events.md) for the complete list of event types. + +--- + +## Threading (CRITICAL) + +**UI operations MUST run on the world thread** or the game will crash. + +### For Commands + +```java +public class MyCommand extends AbstractAsyncCommand { + @Override + protected CompletableFuture executeAsync(CommandContext context) { + if (context.sender() instanceof Player player) { + Ref ref = player.getReference(); + Store store = ref.getStore(); + World world = store.getExternalData().getWorld(); + + return CompletableFuture.runAsync(() -> { + PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType()); + // Open page on world thread + player.getPageManager().setPage(ref, store, new MyPage(playerRef)); + }, world); + } + return CompletableFuture.completedFuture(null); + } +} +``` + +### For HUDs + +```java +World world = store.getExternalData().getWorld(); +world.execute(() -> { + MyHud hud = new MyHud(playerRef); + hud.show(); +}); +``` + +--- + +## Value Objects Reference + +### ItemGridSlot + +```java +new ItemGridSlot() + .setItemStack(new ItemStack(itemId, quantity)) + .setBackground(Value.of(patchStyle)) + .setOverlay(Value.of(overlayStyle)) + .setIcon(Value.of(iconStyle)) + .setName("Custom Name") + .setDescription("Custom description") + .setItemIncompatible(false) + .setActivatable(true) + .setItemUncraftable(false); +``` + +### DropdownEntryInfo + +```java +new DropdownEntryInfo(LocalizableString.fromString("Option 1"), "value1") +``` + +### LocalizableString + +```java +// Plain string +LocalizableString.fromString("Hello World") + +// Localization key +LocalizableString.fromMessageId("server.ui.myKey") + +// With parameters +LocalizableString.fromMessageId("server.ui.greeting", Map.of("name", playerName)) +``` + +--- + +## Checklist + +1. ✅ Place .ui files in `resources/Common/UI/Custom/` +2. ✅ Add `"IncludesAssetPack": true` to `manifest.json` +3. ✅ Image files must end with `@2x.png` +4. ✅ Run UI operations on world thread +5. ✅ Call `sendUpdate()` after handling events in InteractiveCustomUIPage +6. ✅ Use proper selectors with `#` prefix +7. ✅ Use MultipleHUD for multiple HUDs per player +8. ✅ Use namespaced IDs for HUDs (e.g., `hyforged:my_hud`) diff --git a/skills/hytale-ui-modding/references/layout.md b/skills/hytale-ui-modding/references/layout.md new file mode 100644 index 0000000..afbd7c6 --- /dev/null +++ b/skills/hytale-ui-modding/references/layout.md @@ -0,0 +1,526 @@ +# Layout Reference + +The layout system determines how UI elements are positioned and sized on screen. Understanding layout is crucial for creating well-structured, responsive interfaces. + +## Layout Fundamentals + +Every UI element has four key layout concepts: + +1. **Container Rectangle** - The space allocated by the parent element +2. **Anchor** - How the element positions and sizes itself within the container +3. **Padding** - Inner spacing that affects where children are positioned +4. **LayoutMode** - How the element arranges its children (if it's a container) + +Visual representation: + +``` +┌─────────────────────────────────────┐ +│ Container Rectangle (from parent) │ +│ ┌───────────────────────────────┐ │ +│ │ Anchored Rectangle │ │ +│ │ ┌─────────────────────────┐ │ │ +│ │ │ Padding │ │ │ +│ │ │ ┌───────────────────┐ │ │ │ +│ │ │ │ Content Area │ │ │ │ +│ │ │ └───────────────────┘ │ │ │ +│ │ └─────────────────────────┘ │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +--- + +## Anchor + +Anchor controls how an element positions and sizes itself within its container rectangle. + +### Anchor Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Left` | int | Distance from container's left edge | +| `Right` | int | Distance from container's right edge | +| `Top` | int | Distance from container's top edge | +| `Bottom` | int | Distance from container's bottom edge | +| `Width` | int | Fixed width in pixels | +| `Height` | int | Fixed height in pixels | +| `MinWidth` | int | Minimum width constraint | +| `MaxWidth` | int | Maximum width constraint | +| `MinHeight` | int | Minimum height constraint | +| `MaxHeight` | int | Maximum height constraint | + +### Shorthand Properties + +| Shorthand | Expands To | +|-----------|------------| +| `Full` | Left, Top, Right, Bottom (all sides) | +| `Horizontal` | Left and Right | +| `Vertical` | Top and Bottom | + +### Fixed Size + +Creates an element with explicit dimensions: + +```ui +Button { + Anchor: (Width: 200, Height: 40); +} +``` + +Result: A 200×40 pixel button. + +### Positioning + +Position an element at specific offsets from the container edges: + +```ui +Label { + Anchor: (Top: 10, Left: 20, Width: 100, Height: 30); +} +``` + +- Top: 10 pixels from container's top edge +- Left: 20 pixels from container's left edge +- Width: 100 pixels wide +- Height: 30 pixels tall + +### Anchoring to Edges + +Anchor to bottom-right corner: + +```ui +Button { + Anchor: (Bottom: 10, Right: 10, Width: 100, Height: 30); +} +``` + +Anchors the button 10 pixels from the bottom and right edges. + +### Stretching + +Fill the entire container: + +```ui +Group { + Anchor: (Top: 0, Bottom: 0, Left: 0, Right: 0); +} +``` + +Or use the shorthand: + +```ui +Group { + Anchor: (Full: 0); +} +``` + +Stretch with margins: + +```ui +Group { + Anchor: (Full: 10); +} +``` + +This creates 10 pixels of margin on all sides. + +### Mixed Anchoring + +Combine fixed dimensions with stretching: + +```ui +Panel { + Anchor: (Top: 10, Bottom: 10, Left: 20, Width: 300); +} +``` + +- Fixed width of 300 pixels +- Stretches vertically between top and bottom edges +- 10 pixels from top and bottom +- 20 pixels from left + +--- + +## Padding + +Padding creates inner spacing, affecting where children are positioned. + +### Uniform Padding + +Apply the same padding to all sides: + +```ui +Group { + Padding: (Full: 20); +} +``` + +Result: 20 pixels of padding on all sides. + +### Directional Padding + +Different padding per edge: + +```ui +Group { + Padding: (Top: 10, Bottom: 20, Left: 15, Right: 15); +} +``` + +### Shorthand + +Combine horizontal and vertical: + +```ui +Group { + Padding: (Horizontal: 20, Vertical: 10); +} +// Equivalent to: +// Top: 10, Bottom: 10, Left: 20, Right: 20 +``` + +### Effect on Children + +When a child uses `Anchor: (Full: 0)`, it fills the parent but respects padding: + +```ui +Group { + Anchor: (Width: 200, Height: 100); + Padding: (Full: 10); + Label { + Anchor: (Full: 0); + } +} +``` + +Visual result: + +``` +┌──────────────────────┐ +│ Group (200×100) │ +│ ┌────────────────┐ │ +│ │ Label │ │ ← 10px padding all around +│ │ │ │ +│ └────────────────┘ │ +└──────────────────────┘ +``` + +--- + +## LayoutMode + +LayoutMode determines how a container arranges its children. + +### Top (Vertical Stack) + +Children stack vertically from top to bottom: + +```ui +Group { + LayoutMode: Top; + Button { Anchor: (Height: 30); } + Button { Anchor: (Height: 30); } + Button { Anchor: (Height: 30); } +} +``` + +Result: + +``` +┌──────────┐ +│ Button 1 │ +├──────────┤ +│ Button 2 │ +├──────────┤ +│ Button 3 │ +└──────────┘ +``` + +**Spacing:** Use `Anchor.Bottom` to add spacing after each element: + +```ui +Button { Anchor: (Height: 30, Bottom: 10); } // 10px gap after this button +``` + +### Bottom (Vertical Stack, Bottom-Aligned) + +Children stack vertically but align to the bottom edge: + +```ui +Group { + LayoutMode: Bottom; + Button { Anchor: (Height: 30); } + Button { Anchor: (Height: 30); } + Button { Anchor: (Height: 30); } +} +``` + +### Left (Horizontal Stack) + +Children arrange horizontally from left to right: + +```ui +Group { + LayoutMode: Left; + Button { Anchor: (Width: 80); } + Button { Anchor: (Width: 80); } + Button { Anchor: (Width: 80); } +} +``` + +Result: + +``` +┌────────┬────────┬────────┐ +│ Button │ Button │ Button │ +│ 1 │ 2 │ 3 │ +└────────┴────────┴────────┘ +``` + +**Spacing:** Use `Anchor.Right` for spacing between elements. + +### Right (Horizontal Stack, Right-Aligned) + +Children arrange horizontally, aligned to the right side of the parent: + +```ui +Group { + LayoutMode: Right; + Button { Anchor: (Width: 80); } + Button { Anchor: (Width: 80); } + Button { Anchor: (Width: 80); } +} +``` + +### Center + +Centers children horizontally within the parent: + +```ui +Group { + LayoutMode: Center; + Group #Dialog { + Anchor: (Width: 400, Height: 300); + } +} +``` + +### Middle + +Centers children vertically within the parent: + +```ui +Group { + LayoutMode: Middle; + Group #Dialog { + Anchor: (Width: 400, Height: 300); + } +} +``` + +### CenterMiddle (Horizontal Stack, Fully Centered) + +Children stack horizontally from left to right, centered both horizontally and vertically: + +```ui +Group { + LayoutMode: CenterMiddle; + Button { Anchor: (Width: 80); } + Button { Anchor: (Width: 80); } + Button { Anchor: (Width: 80); } +} +``` + +Result: + +``` +┌────────────────────────────────────────────┐ +│ │ +│ │ +│ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ B1 │ │ B2 │ │ B3 │ │ +│ └──────┘ └──────┘ └──────┘ │ +│ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### MiddleCenter (Vertical Stack, Fully Centered) + +Children stack vertically from top to bottom, centered both horizontally and vertically: + +```ui +Group { + LayoutMode: MiddleCenter; + Button { Anchor: (Height: 30); } + Button { Anchor: (Height: 30); } + Button { Anchor: (Height: 30); } +} +``` + +Result: + +``` +┌────────────────────────────────────────────┐ +│ │ +│ ┌──────────┐ │ +│ │ Button 1 │ │ +│ ├──────────┤ │ +│ │ Button 2 │ │ +│ ├──────────┤ │ +│ │ Button 3 │ │ +│ └──────────┘ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### Full (Absolute Positioning) + +Children use absolute positioning via their Anchor properties: + +```ui +Group { + LayoutMode: Full; + Label { + Anchor: (Top: 20, Left: 20, Width: 100, Height: 30); + } +} +``` + +### TopScrolling / BottomScrolling + +Like Top/Bottom, but adds a scrollbar if content exceeds container height: + +```ui +Group { + LayoutMode: TopScrolling; + ScrollbarStyle: $Common.@DefaultScrollbar; + // ... many children +} +``` + +### LeftScrolling / RightScrolling + +Like Left/Right, but adds a scrollbar for horizontal scrolling: + +```ui +Group { + LayoutMode: LeftScrolling; + ScrollbarStyle: $Common.@DefaultScrollbar; + // ... many children +} +``` + +### LeftCenterWrap (Wrapping Horizontal Stack) + +Children flow left to right. When there's no more horizontal space, they wrap to the next row. Each row is horizontally centered: + +```ui +Group { + LayoutMode: LeftCenterWrap; + Button { Anchor: (Width: 80, Height: 30); } + Button { Anchor: (Width: 80, Height: 30); } + Button { Anchor: (Width: 80, Height: 30); } + Button { Anchor: (Width: 80, Height: 30); } + Button { Anchor: (Width: 80, Height: 30); } +} +``` + +Result: + +``` +┌────────────────────────────────────────────┐ +│ │ +│ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ B1 │ │ B2 │ │ B3 │ │ +│ └──────┘ └──────┘ └──────┘ │ +│ ┌──────┐ ┌──────┐ │ +│ │ B4 │ │ B5 │ │ +│ └──────┘ └──────┘ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### Complete LayoutMode Reference + +| Mode | Direction | Alignment | Scrollable Variant | +|------|-----------|-----------|-------------------| +| `Top` | Vertical | Top | `TopScrolling` | +| `Bottom` | Vertical | Bottom | `BottomScrolling` | +| `Left` | Horizontal | Left | `LeftScrolling` | +| `Right` | Horizontal | Right | `RightScrolling` | +| `Center` | - | Horizontal center | - | +| `Middle` | - | Vertical center | - | +| `CenterMiddle` | Horizontal | Both centered | - | +| `MiddleCenter` | Vertical | Both centered | - | +| `Full` | Absolute | Via Anchor | - | +| `LeftCenterWrap` | Horizontal wrap | Centered rows | - | +| `RightCenterWrap` | Horizontal wrap | Centered rows | - | + +--- + +## FlexWeight + +FlexWeight distributes remaining space among children after fixed-size elements are placed. + +### Basic Usage + +```ui +Group { + LayoutMode: Left; + Anchor: (Width: 400); + Button { + Anchor: (Width: 100); + } + Group { + FlexWeight: 1; // Takes all remaining space + } + Button { + Anchor: (Width: 100); + } +} +``` + +Result: +- First button: 100px +- Middle group: 200px (400 - 100 - 100 = 200) +- Last button: 100px + +### Multiple FlexWeights + +When multiple elements have FlexWeight, space is distributed proportionally: + +```ui +Group { + LayoutMode: Left; + Anchor: (Width: 600); + Group { FlexWeight: 1; } + Group { FlexWeight: 2; } + Group { FlexWeight: 1; } +} +``` + +Remaining space (600px) is split: +- First group: 600 × (1/4) = 150px +- Second group: 600 × (2/4) = 300px +- Third group: 600 × (1/4) = 150px + +--- + +## Visibility + +Control whether an element is displayed: + +```ui +Button #HiddenButton { + Visible: false; +} +``` + +Effect: +- Element and its children are not displayed +- Element is **not** included in layout (doesn't take up space) + +--- + +Source: https://hytalemodding.dev/en/docs/official-documentation/custom-ui/layout diff --git a/skills/hytale-ui-modding/references/markup.md b/skills/hytale-ui-modding/references/markup.md new file mode 100644 index 0000000..c070d29 --- /dev/null +++ b/skills/hytale-ui-modding/references/markup.md @@ -0,0 +1,273 @@ +# Markup Reference + +A UI document (.ui file) contains trees of elements. There can be multiple root elements in a single document. + +## Basic Syntax + +An element is the basic building block of a user interface. Here's the fundamental syntax: + +```ui +// Basic declaration of an element +// with its Anchor property set to attach on all sides of its parent +// with 10 pixels of margin +Group { Anchor: (Left: 10, Top: 10, Right: 10, Bottom: 10); } +Group { Anchor: (Full: 10); } // More concise version + +// Declaration of a Label with a name +// (can be used to access the element from game code) +Label #MyLabel { + Style: LabelStyle(FontSize: 16); // or just Style: (FontSize: 16), type can be inferred. + Text: "Hi! I am text."; +} + +// Declaration of a Group containing 2 children +Group { + LayoutMode: Left; + Label { Text: "Child 1"; FlexWeight: 2; } + Label { Text: "Child 2"; FlexWeight: 1; } +} +``` + +### Syntax Rules + +- **Elements**: `ElementType [#Id] { properties... children... }` +- **IDs**: Prefix with `#` for Java/event access (e.g., `#MyLabel`) +- **Properties**: `PropertyName: value;` +- **Comments**: `// single line comment` +- **Tuples/Objects**: `(Key1: value1, Key2: value2)` + +--- + +## Documents + +A UI document can have multiple root elements. Each root element becomes a separate tree in the UI hierarchy. + +--- + +## Named Expressions + +Named expressions are reusable values declared with the `@` prefix. They must be declared at the top of the block (before properties and children). + +### Basic Named Expressions + +```ui +// Example of named expressions, declared and used with @ prefix +@Title = "Hytale"; +@ExtraSpacing = 5; + +Label { + Text: @Title; + Style: (LetterSpacing: 2 + @ExtraSpacing); +} +``` + +### Named Expression Scoping + +Named expressions are scoped to the subtree where they are declared. They can be declared at any level, including document root. + +### Spread Operator + +Use the spread operator `...` to reuse a named expression while overriding some of its fields: + +```ui +@MyBaseStyle = LabelStyle(FontSize: 24, LetterSpacing: 2); + +Label { + Style: (...@MyBaseStyle, FontSize: 36); +} +``` + +### Layering Multiple Named Expressions + +You can combine multiple named expressions: + +```ui +@TitleStyle = LabelStyle(FontSize: 24, HorizontalAlignment: Center); +@BigTextStyle = LabelStyle(FontSize: 36); +@SpacedTextStyle = LabelStyle(LetterSpacing: 2); + +Label { + Style: (...@BigTextStyle, ...@SpacedTextStyle); +} +``` + +### Document References + +A document can reference another document and access its named expressions using the `$` prefix: + +```ui +// Document references are defined with $ prefix +$Common = "../Common.ui"; + +TextButton { + Style: $Common.@DefaultButtonStyle; +} +``` + +--- + +## Templates + +Templates are named expressions that contain element trees. You can instantiate them multiple times with customizations. + +### Declaring and Using Templates + +```ui +// This is the template +@Row = Group { + Anchor: (Height: 50); + Label #Label { Anchor: (Left: 0, Width: 100); Text: @LabelText; } + Group #Content { Anchor: (Left: 100); } +}; + +// Here we'll be using it twice in the document tree +Group #Rows { + LayoutMode: TopScrolling; + @Row #MyFirstRow { + @LabelText = "First row"; + #Content { TextField {} } + } + @Row #MySecondRow { + @LabelText = "Second row"; + } +} +``` + +### Template Customization Rules + +- You can override local named expressions inside template instances +- You can insert additional children at any point in the template tree by targeting the child's ID +- Local named expressions must be defined at the very top of the block, before properties and child elements + +--- + +## Property Types + +### Basic Types + +| Type | Example | Notes | +|------|---------|-------| +| Boolean | `Visible: false;` | `true` or `false` | +| Int | `Height: 20;` | Whole numbers | +| Float, Double, Decimal | `Min: 0.2;` | Decimal numbers | +| String | `Text: "Hi!";` | Quoted text | +| Char | `PasswordChar: "*";` | Single character only (same syntax as string) | +| Color | `Background: #ffffff;` | Hex color literals | +| Object | `Style: (Background: #ffffff)` | Parentheses with key-value pairs | +| Array | `TextSpans: [(Text: "Hi", IsBold: true)]` | Square brackets with objects | + +### Translations + +Translation keys can be referenced anywhere you can provide a string. They are converted to localized strings when the element is instantiated: + +```ui +Label { + Text: %ui.general.cancel; +} +``` + +The translation key uses the `%` prefix and references keys from language files. + +### Colors + +Color literals can be written in several formats: + +| Format | Description | Example | +|--------|-------------|---------| +| `#rrggbb` | 6-digit hex (fully opaque) | `#ffffff` | +| `#rrggbb(a.a)` | 6-digit hex with alpha (0-1) | `#000000(0.3)` | +| `#rrggbbaa` | 8-digit hex with alpha | `#ffffff80` | + +**Preferred:** The `#rrggbb(a.a)` format is recommended for readability. + +```ui +Group { + Background: #000000(0.3); // 30% opacity black +} +``` + +### Font Names + +Font names are strings that map to `UIFontName` internally: + +```ui +Label { + Text: "Hi"; + Style: (FontName: "Secondary"); +} +``` + +**Available Font Names:** + +| Name | Use Case | +|------|----------| +| `Default` | Standard text; used unless specified otherwise | +| `Secondary` | Headlines or elements that should stand out | +| `Mono` | Development only (profiling, error overlays) | + +### Paths (UIPath) + +Paths reference other UI assets and are always relative to the current file location: + +```ui +// UIPath syntax is the same as String +Sprite { + TexturePath: "MyButton.png"; +} +``` + +**Path Resolution Examples:** + +| Reference | Current File | Resolved Path | +|-----------|--------------|---------------| +| `MyButton.png` | `Menu/MyAwesomeMenu.ui` | `Menu/MyButton.png` | +| `../MyButton.png` | `Menu/MyAwesomeMenu.ui` | `MyButton.png` | +| `../../MyButton.png` | `Menu/Popup/Templates/MyAwesomeMenu.ui` | `Menu/MyButton.png` | + +### Objects + +Objects contain a set of properties in parentheses: + +```ui +Group { + Anchor: ( + Height: 10, + Width: 20 + ); +} +``` + +Type inference is supported. If the property type is known, you don't need to specify the type name: + +```ui +// Explicit type +Style: LabelStyle(FontSize: 16); + +// Inferred type (when property expects LabelStyle) +Style: (FontSize: 16); +``` + +### Arrays + +Arrays use square brackets and contain objects: + +```ui +Label { + TextSpans: [ + (Text: "Hello ", IsBold: true), + (Text: "World", IsBold: false) + ]; +} +``` + +--- + +## Visual Studio Code Extension + +There is an official VS Code extension that adds syntax highlighting for `.ui` files: + +https://marketplace.visualstudio.com/items?itemName=HypixelStudiosCanadaInc.vscode-hytaleui + +--- + +Source: https://hytalemodding.dev/en/docs/official-documentation/custom-ui/markup diff --git a/skills/hytale-ui-modding/references/overview.md b/skills/hytale-ui-modding/references/overview.md new file mode 100644 index 0000000..355f04b --- /dev/null +++ b/skills/hytale-ui-modding/references/overview.md @@ -0,0 +1,128 @@ +```markdown +# Custom UI Overview + +Custom UI is Hytale's framework for creating **custom user interfaces** controlled by the game server. Unlike the built-in Client UI (which is part of the game client and cannot be modified), Server UI allows you to create interactive screens and HUD overlays through Java plugins and asset packs. + +## What You Can Do + +- **Create custom interactive pages** - Shop interfaces, quest dialogs, server settings menus, admin panels +- **Add custom HUD overlays** - Quest trackers, status displays, custom health bars, server information +- **Design with markup** - Use `.ui` files to define reusable UI templates +- **Handle user interactions** - Respond to button clicks, form submissions, and other events +- **Localize your UI** - Support multiple languages using the game's translation system + +--- + +## How Custom UI Fits Into Hytale's UI + +Hytale's user interface is divided into two categories: + +### Client UI (Not Moddable) + +Built-in interfaces controlled by the C# game client: + +- Main menu and settings +- Character creation +- Built-in HUD (health, hotbar, chat) +- Inventory and crafting screens +- Development tools + +**You cannot modify these** - they are part of the core game client. + +### In-Game UI (Moddable via Server) + +Server-controlled interfaces that you can create and customize: + +#### Custom Pages + +Full-screen interactive overlays that appear during gameplay: + +- Can be dismissed by the player (ESC key) +- Capture all input (keyboard and mouse) +- Support loading states while waiting for server responses +- Perfect for: shops, dialogs, menus, configuration screens + +#### Custom HUDs + +Persistent overlay elements drawn on top of the game world: + +- Display-only (no user interaction) +- Always visible during gameplay +- Lightweight and non-intrusive +- Perfect for: quest objectives, status indicators, server info panels + +--- + +## Architecture Overview + +Server UI uses a **command-based architecture**: + +``` +┌─────────────────────┐ ┌──────────────────────┐ +│ Java Server │ │ C# Client │ +│ (Your Plugin) │ │ (Game) │ +├─────────────────────┤ ├──────────────────────┤ +│ │ │ │ +│ InteractiveCustomUI │ │ CustomPage or │ +│ Page │ │ CustomHud │ +│ ↓ build() ├────────→ │ ↓ Apply │ +│ UICommandBuilder │ │ Element Tree │ +│ - append() │ │ ↓ Layout │ +│ - set() │ │ Rendered UI │ +│ - clear() │ │ │ +│ │ │ │ +│ handleDataEvent() │←─────────│ User Interaction │ +│ Process input │ Events │ (click, type, etc) │ +│ sendUpdate() │ │ │ +└─────────────────────┘ └──────────────────────┘ +``` + +**The flow:** + +1. Your Java code builds UI using `UICommandBuilder` +2. Commands are sent to the client as data +3. Client parses `.ui` markup files and creates visual elements +4. User interacts with the UI +5. Events are sent back to your Java code +6. You process events and send updates back + +--- + +## Key Principles + +### Declarative, Not Imperative + +You don't create UI objects directly. Instead, you send **commands** that describe what you want: + +- "Append this button template to that container" +- "Set this label's text to 'Hello World'" +- "Clear all children from this list" + +### Asset-Driven + +UI structure is defined in `.ui` markup files (assets), not hardcoded in Java. This enables: + +- Designers to modify layouts without touching code +- Reusable UI components +- Consistent visual language + +### Event-Driven + +User interactions trigger events that flow back to your server code. You register event bindings and handle them in `handleDataEvent()`. + +### Selector-Based + +You target specific UI elements using **selectors:** + +| Selector | Meaning | +|----------|---------| +| `#MyButton` | Element with ID "MyButton" | +| `#List[0]` | First child of element "List" | +| `#List[0] #Title` | Element "Title" in the first child of "List" | +| `#Label.TextColor` | The TextColor property of element "Label" | + +--- + +Source: https://hytalemodding.dev/en/docs/official-documentation/custom-ui + +``` diff --git a/skills/hytale-ui-modding/references/translations.md b/skills/hytale-ui-modding/references/translations.md new file mode 100644 index 0000000..a6f86b2 --- /dev/null +++ b/skills/hytale-ui-modding/references/translations.md @@ -0,0 +1,184 @@ +# Translations & Localization + +This document covers localization in Hytale UI - both translating your mod's UI text and contributing to the HytaleModding community translations. + +--- + +## Translating Your Mod's UI + +### Translation Keys in .ui Files + +Use the `%` prefix to reference translation keys in your UI markup: + +```ui +Label { + Text: %ui.mymod.greeting; +} + +Button { + Text: %ui.mymod.button.save; +} +``` + +### Language Files + +Translation strings are stored in `.lang` files under `Server/Languages/`: + +``` +src/main/resources/ +└── Server/ + └── Languages/ + ├── en-US/ + │ └── ui.lang + ├── es-ES/ + │ └── ui.lang + └── fallback.lang +``` + +### Language File Format + +Language files use a simple key-value format: + +```properties +# en-US/ui.lang +ui.mymod.greeting = Hello, adventurer! +ui.mymod.button.save = Save +ui.mymod.button.cancel = Cancel +ui.mymod.inventory.title = Inventory +``` + +```properties +# es-ES/ui.lang +ui.mymod.greeting = ¡Hola, aventurero! +ui.mymod.button.save = Guardar +ui.mymod.button.cancel = Cancelar +ui.mymod.inventory.title = Inventario +``` + +### Fallback Configuration + +The `fallback.lang` file maps locales to their fallback: + +```properties +# fallback.lang +en-GB = en-US +es-MX = es-ES +pt-BR = pt-PT +``` + +### Using Translations in Java + +```java +import com.hypixel.hytale.server.core.i18n.LocalizableString; +import com.hypixel.hytale.server.core.i18n.Message; + +// From a translation key +LocalizableString text = LocalizableString.fromMessageId("ui.mymod.greeting"); + +// With parameters +LocalizableString text = LocalizableString.fromMessageId( + "ui.mymod.welcome", + Map.of("name", playerName) +); + +// In language file: +// ui.mymod.welcome = Welcome, {name}! + +// Plain string (no translation) +LocalizableString text = LocalizableString.fromString("Static text"); + +// Using Message class for chat +Message.translation("chat.mymod.joined", Map.of("player", playerName)); +``` + +### Translation Parameters + +Use `{paramName}` for dynamic values: + +```properties +# Language file +ui.mymod.level = Level: {level} +ui.mymod.damage = You dealt {amount} damage to {target}! +``` + +```ui +Label { + Text: %ui.mymod.level; +} +``` + +Set the parameter value from Java: + +```java +UICommandBuilder cmd = new UICommandBuilder(); +cmd.set("#LevelLabel.Text", + LocalizableString.fromMessageId("ui.mymod.level", Map.of("level", String.valueOf(playerLevel)))); +``` + +### Best Practices + +1. **Use namespaced keys** - Prefix with your mod name: `ui.mymod.feature.key` +2. **Keep keys descriptive** - `ui.mymod.inventory.empty` not `ui.mymod.ie` +3. **Externalize all user-facing text** - Never hardcode display strings +4. **Support parameters** - Use `{param}` for dynamic content +5. **Provide fallback locale** - Always have en-US as base +6. **Test all locales** - Verify translations fit in your UI layouts + +--- + +## Contributing to HytaleModding Translations + +Help translate the HytaleModding documentation website to your language. + +### How to Contribute + +All translations are managed via Crowdin: + +1. Visit [translate.hytalemodding.dev](https://translate.hytalemodding.dev/) +2. Log in (create account if needed) +3. Click on your language +4. Click on the file/article you wish to translate +5. Start translating! +6. Approved translations will appear on the website + +### Translation Guidelines + +- **Be confident** - You should be able to read your translation and understand the meaning easily +- **Keep it simple** - Use the simplest form of language possible +- **Use English words** if the translation is unknown to the majority in your country +- **Use your imagination** - Feel free to change context as long as meaning is preserved +- **Follow Hytale's official translations** when available + +### What NOT to Translate + +**Callout types** - Only translate title and content: +```mdx + + Translate the text in between! + +``` +Do NOT translate "warning" - it's a technical identifier. + +**Icon names** - These are technical identifiers: +```mdx +icon: Globe +``` +Translating icon names will break icon rendering. + +**Code blocks** - Keep code samples in their original form. + +### Discussion & Support + +1. Join the [HytaleModding Discord](https://discord.gg/hytalemodding) +2. Open the `#translation` channel for general translation discussion +3. Run `/translator ` to join your language's thread +4. If your language isn't available: + - Request it on Crowdin if not listed + - Ping **Neil** on Discord if it's on Crowdin but missing from the bot + +--- + +## Source + +- Official UI Markup: https://hytalemodding.dev/en/docs/official-documentation/custom-ui/markup +- Translation Guidelines PR: https://github.com/HytaleModding/site/pull/396 diff --git a/skills/hytale-ui-modding/references/troubleshooting.md b/skills/hytale-ui-modding/references/troubleshooting.md new file mode 100644 index 0000000..db41df2 --- /dev/null +++ b/skills/hytale-ui-modding/references/troubleshooting.md @@ -0,0 +1,11 @@ +# Troubleshooting + +## Common issues + +- Failed to apply Custom UI HUD commands: syntax error in .ui. Enable Diagnostic Mode in Hytale settings. +- Could not find document for Custom UI Append: wrong path or file not under Common/UI/Custom/. +- Unknown node type: unsupported or misspelled element type. +- Page stuck on Loading: missing sendUpdate in interactive page event handling. +- Client disconnect when opening UI: UI work not on world thread. +- Texture missing: wrong path or missing @2x.png suffix. +- Events not firing: selector does not match element ID. diff --git a/skills/hytale-ui-modding/references/type-documentation.md b/skills/hytale-ui-modding/references/type-documentation.md new file mode 100644 index 0000000..32ca3f0 --- /dev/null +++ b/skills/hytale-ui-modding/references/type-documentation.md @@ -0,0 +1,225 @@ +# Type Documentation Reference + +The type documentation is a generated index of all UI elements, property types, and enums available in markup. Use it as your reference when you need exact property names, types, or valid enum values. + +--- + +## Elements + +Elements are the building blocks of UI. Each element type has specific properties. + +### Commonly Used Elements + +| Element | Purpose | Key Properties | +|---------|---------|----------------| +| `Group` | Container/layout | `LayoutMode`, `Background`, `Padding`, `ScrollbarStyle` | +| `Label` | Text display | `Text`, `TextSpans`, `Style`, `TextColor` | +| `Button` | Clickable button | `Text`, `Disabled`, `Style`, `Background` | +| `TextField` | Text input | `Value`, `PlaceholderText`, `MaxLength`, `ReadOnly`, `Password`, `PasswordChar`, `AutoGrow`, `MaxVisibleLines` | +| `Slider` | Range input | `Value`, `Min`, `Max`, `Step`, `Style` | +| `CheckBox` | Toggle input | `Value` (boolean) | +| `DropdownBox` | Dropdown selection | `Value`, `Entries` (DropdownEntryInfo[]) | +| `ProgressBar` | Progress display | `Value` (0.0-1.0), `BarTexturePath`, `EffectTexturePath`, `Direction`, `Alignment`, `Color` | +| `CircularProgressBar` | Circular progress | `Value`, `MaskTexturePath` | +| `Sprite` | Animated image | `TexturePath`, `Frame`, `FramesPerSecond` | +| `ItemIcon` | Item display | `ItemId`, `Quantity` | +| `ItemSlot` | Full item slot | `ItemStack`, `Background`, `Overlay`, `Icon` | +| `ItemGrid` | Scrollable item grid | `Slots`, `SlotsPerRow`, `AreItemsDraggable`, `ShowScrollbar`, `KeepScrollPosition`, `RenderItemQualityBackground` | +| `TabNavigation` | Tab bar | Works with tab content groups | +| `TimerLabel` | Timer display | Specialized label for timers | + +### Full Element List + +- ActionButton +- AssetImage +- BackButton +- BlockSelector +- Button +- CharacterPreviewComponent +- CheckBox +- CheckBoxContainer +- CircularProgressBar +- CodeEditor +- ColorOptionGrid +- ColorPicker +- ColorPickerDropdownBox +- CompactTextField +- DropdownBox +- DropdownEntry +- DynamicPane +- DynamicPaneContainer +- FloatSlider +- FloatSliderNumberField +- Group +- HotkeyLabel +- ItemGrid +- ItemIcon +- ItemPreviewComponent +- ItemSlot +- ItemSlotButton +- Label +- LabeledCheckBox +- MenuItem +- MultilineTextField +- NumberField +- Panel +- ProgressBar +- ReorderableList +- ReorderableListGrip +- SceneBlur +- Slider +- SliderNumberField +- Sprite +- TabButton +- TabNavigation +- TextButton +- TextField +- TimerLabel +- ToggleButton + +--- + +## Property Types + +Property types define the structure of complex values used in element properties. + +### Commonly Used Property Types + +| Type | Purpose | +|------|---------| +| `Anchor` | Element positioning and sizing | +| `Padding` | Inner spacing | +| `PatchStyle` | Nine-slice scalable backgrounds | +| `ScrollbarStyle` | Scrollbar appearance | +| `LabelStyle` | Text styling (font, size, color, alignment) | +| `ButtonStyle` | Button visual states | +| `SliderStyle` | Slider appearance | +| `TabStyle` | Tab appearance | +| `TabNavigationStyle` | Tab bar styling | +| `TextButtonStyle` | Text button styling | +| `ItemGridSlot` | Item grid slot data | +| `LabelSpan` | Rich text span | + +### Full Property Type List + +- Anchor +- BlockSelectorStyle +- ButtonSounds +- ButtonStyle +- ButtonStyleState +- CheckBoxStyle +- CheckBoxStyleState +- ClientItemStack +- ColorOptionGridStyle +- ColorPickerDropdownBoxStateBackground +- ColorPickerDropdownBoxStyle +- ColorPickerStyle +- DropdownBoxSearchInputStyle +- DropdownBoxSounds +- DropdownBoxStyle +- InputFieldButtonStyle +- InputFieldDecorationStyle +- InputFieldDecorationStyleState +- InputFieldIcon +- InputFieldStyle +- ItemGridSlot +- ItemGridStyle +- LabeledCheckBoxStyle +- LabeledCheckBoxStyleState +- LabelSpan +- LabelStyle +- NumberFieldFormat +- Padding +- PatchStyle +- PopupStyle +- ScrollbarStyle +- SliderStyle +- SoundStyle +- SpriteFrame +- SubMenuItemStyle +- SubMenuItemStyleState +- Tab +- TabNavigationStyle +- TabStyle +- TabStyleState +- TextButtonStyle +- TextButtonStyleState +- TextTooltipStyle +- ToggleButtonStyle +- ToggleButtonStyleState + +--- + +## Enums + +Enums define valid values for certain properties. + +### Commonly Used Enums + +| Enum | Values | Purpose | +|------|--------|---------| +| `LayoutMode` | Top, Bottom, Left, Right, Center, Middle, CenterMiddle, MiddleCenter, Full, TopScrolling, BottomScrolling, LeftScrolling, RightScrolling, LeftCenterWrap, RightCenterWrap | How a container arranges children | +| `LabelAlignment` | Left, Right, Center | Text horizontal alignment | +| `ProgressBarDirection` | LeftToRight, RightToLeft, BottomToTop, TopToBottom | Progress bar fill direction | +| `ProgressBarAlignment` | Start, Center, End | Progress bar alignment | +| `TimerDirection` | Up, Down | Timer count direction | +| `ResizeType` | None, Horizontal, Vertical, Both | Resize behavior | + +### Full Enum List + +- ActionButtonAlignment +- CodeEditorLanguage +- ColorFormat +- DropdownBoxAlign +- InputFieldButtonSide +- InputFieldIconSide +- ItemGridInfoDisplayMode +- LabelAlignment +- LayoutMode +- MouseWheelScrollBehaviourType +- ProgressBarAlignment +- ProgressBarDirection +- ResizeType +- TimerDirection +- TooltipAlignment + +--- + +## How to Use Type Documentation + +When you need specific details: + +1. **Find the element type** you're working with +2. **Look up its properties** in the element documentation +3. **Check the property type** to understand what structure is expected +4. **Reference enums** for valid values when a property expects an enum + +### Example: ProgressBar + +To create a progress bar: + +```ui +ProgressBar #HealthBar { + Anchor: (Width: 200, Height: 20); + Value: 0.75; + Direction: LeftToRight; // ProgressBarDirection enum + Alignment: Start; // ProgressBarAlignment enum + Color: #22cc22; +} +``` + +--- + +## Online Reference + +For the most up-to-date and detailed documentation, visit: +https://hytalemodding.dev/en/docs/official-documentation/custom-ui/type-documentation + +The online documentation includes: +- Complete property lists for each element +- Detailed descriptions of each property type +- All valid enum values with descriptions + +--- + +Source: https://hytalemodding.dev/en/docs/official-documentation/custom-ui/type-documentation diff --git a/skills/hytale-ui-modding/references/types.md b/skills/hytale-ui-modding/references/types.md new file mode 100644 index 0000000..472d9bf --- /dev/null +++ b/skills/hytale-ui-modding/references/types.md @@ -0,0 +1,779 @@ +# UI Types Reference + +Complete reference of all UI elements, property types, and enums available in Hytale UI markup. + +--- + +## Elements + +Elements are the building blocks of UI. Each element has specific properties and event callbacks. + +### Base Properties (Inherited by All Elements) + +These properties are available on virtually all elements: + +| Property | Type | Description | +|----------|------|-------------| +| `Visible` | Boolean | Hides the element. Makes parent layouting skip this element as well | +| `HitTestVisible` | Boolean | If true, element will be returned during HitTest (enables click detection) | +| `TooltipText` | String | Enables a text tooltip shown on hover | +| `TooltipTextSpans` | List<LabelSpan> | Tooltip with formatted text spans | +| `TextTooltipStyle` | TextTooltipStyle | Style options for the tooltip | +| `TextTooltipShowDelay` | Float | Delay in seconds before tooltip appears | +| `Anchor` | Anchor | How the element is laid out inside its allocated area | +| `Padding` | Padding | Space around content (background unaffected) | +| `FlexWeight` | Integer | Distribution of remaining space after explicit sizes | +| `Background` | PatchStyle / String | Background image or color | +| `MaskTexturePath` | UI Path (String) | Mask texture for clipping | +| `OutlineColor` | Color | Color for outline | +| `OutlineSize` | Float | Draws outline with specified size | + +--- + +### Group + +**Container element** - Accepts children: Yes + +The fundamental container for laying out child elements. + +| Property | Type | Description | +|----------|------|-------------| +| `LayoutMode` | LayoutMode | How child elements are arranged | +| `ScrollbarStyle` | ScrollbarStyle | Scrollbar appearance | +| `ContentWidth` | Integer | If set, displays horizontal scrollbar | +| `ContentHeight` | Integer | If set, displays vertical scrollbar | +| `AutoScrollDown` | Boolean | Auto-scroll to bottom (unless scrolled up) | +| `KeepScrollPosition` | Boolean | Keep scroll position after unmount | +| `MouseWheelScrollBehaviour` | MouseWheelScrollBehaviourType | Scroll behavior | +| `Overscroll` | Boolean | Extend scrolling areas by element size | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `Validating` | Triggered on Enter key press | +| `Dismissing` | Triggered on Escape key press | +| `Scrolled` | Triggered after scrolling | + +**Example:** +```ui +Group #Container { + LayoutMode: Top; + Padding: (Full: 10); + Background: PatchStyle(Color: #1a1a2eF0, Border: 4); + + Label { Text: "Child 1"; } + Label { Text: "Child 2"; } +} +``` + +--- + +### Label + +**Text display** - Accepts children: No + +Displays text with optional formatting via spans. + +| Property | Type | Description | +|----------|------|-------------| +| `Text` | String | Plain text content | +| `TextSpans` | List<LabelSpan> | Formatted text spans | +| `Style` | LabelStyle | Text styling | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `LinkActivating` | Called when a link is clicked | +| `TagMouseEntered` | Called when hovering a tag | + +**Example:** +```ui +Label #Title { + Text: "Hello World"; + Style: (FontSize: 20, RenderBold: true, TextColor: #ffffff); +} + +// With rich text +Label #RichText { + TextSpans: [ + (Text: "Bold ", IsBold: true), + (Text: "and ", Color: #aaaaaa), + (Text: "Colored", Color: #ff6600) + ]; +} +``` + +--- + +### Button + +**Clickable button** - Accepts children: Yes + +| Property | Type | Description | +|----------|------|-------------| +| `LayoutMode` | LayoutMode | How child elements are arranged | +| `Disabled` | Boolean | Whether button is clickable | +| `Style` | ButtonStyle | Button visual style | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `Activating` | Triggered on click | +| `DoubleClicking` | Triggered on double click | +| `RightClicking` | Triggered on right click | +| `MouseEntered` | Mouse cursor entered bounds | +| `MouseExited` | Mouse cursor left bounds | + +**Example:** +```ui +Button #SaveButton { + Anchor: (Width: 120, Height: 36); + Disabled: false; + Style: $Common.@DefaultButtonStyle; + + Label { Text: "Save"; } +} +``` + +--- + +### TextField + +**Text input** - Accepts children: No + +| Property | Type | Description | +|----------|------|-------------| +| `Value` | String | Current text value | +| `PlaceholderText` | String | Text shown when empty | +| `PasswordChar` | Char | Character to replace text (for passwords) | +| `Style` | InputFieldStyle | Text style | +| `PlaceholderStyle` | InputFieldStyle | Placeholder text style | +| `Decoration` | InputFieldDecorationStyle | Field decoration style | +| `AutoFocus` | Boolean | Auto-focus when mounted | +| `AutoSelectAll` | Boolean | Auto-select all text (requires AutoFocus) | +| `IsReadOnly` | Boolean | Whether editable | +| `MaxLength` | Integer | Maximum character count | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `RightClicking` | Right click on field | +| `Validating` | Enter key pressed | +| `Dismissing` | Escape key pressed | +| `FocusLost` | Field lost focus | +| `FocusGained` | Field gained focus | +| `ValueChanged` | Text value changed | + +**Example:** +```ui +TextField #Username { + Anchor: (Height: 36); + FlexWeight: 1; + PlaceholderText: "Enter username..."; + MaxLength: 32; +} + +TextField #Password { + Anchor: (Height: 36); + PasswordChar: "*"; +} +``` + +--- + +### Slider + +**Range input with draggable handle** - Accepts children: No + +| Property | Type | Description | +|----------|------|-------------| +| `Value` | Integer | Current value | +| `Min` | Integer | Minimum allowed value | +| `Max` | Integer | Maximum allowed value | +| `Step` | Integer | Increment/decrement amount | +| `IsReadOnly` | Boolean | Whether editable | +| `Style` | SliderStyle | Slider style | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `MouseButtonReleased` | Drag completed | +| `ValueChanged` | Value changed | + +**Example:** +```ui +Slider #VolumeSlider { + Anchor: (Height: 24); + FlexWeight: 1; + Value: 50; + Min: 0; + Max: 100; + Step: 1; +} +``` + +--- + +### CheckBox + +**Toggle input** - Accepts children: No + +| Property | Type | Description | +|----------|------|-------------| +| `Value` | Boolean | Checked state | +| `Disabled` | Boolean | Whether clickable | +| `Style` | CheckBoxStyle | CheckBox style | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `ValueChanged` | Checked state changed | + +**Example:** +```ui +CheckBox #EnableSound { + Value: true; +} +``` + +--- + +### DropdownBox + +**Dropdown selection** - Accepts children: Yes + +| Property | Type | Description | +|----------|------|-------------| +| `Entries` | IReadOnlyList | Dropdown entries | +| `SelectedValues` | List<String> | Selected values (multi-select) | +| `Value` | String | Selected value (single-select) | +| `Disabled` | Boolean | Whether clickable | +| `Style` | DropdownBoxStyle | Dropdown style | +| `PanelTitleText` | String | Title for dropdown panel | +| `IsReadOnly` | Boolean | Whether editable | +| `MaxSelection` | Integer | Maximum selections allowed | +| `ShowSearchInput` | Boolean | Show search filter | +| `ShowLabel` | Boolean | Show selected label | +| `ForcedLabel` | String | Override label text | +| `NoItemsText` | String | Text when empty | +| `DisplayNonExistingValue` | Boolean | Show value not in entries | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `ValueChanged` | Selection changed | +| `DropdownToggled` | Dropdown opened/closed | + +**Example:** +```ui +DropdownBox #LanguageSelect { + Anchor: (Width: 200, Height: 36); + Value: "en-US"; + ShowSearchInput: true; +} +``` + +--- + +### ProgressBar + +**Progress display** - Accepts children: No + +| Property | Type | Description | +|----------|------|-------------| +| `Value` | Float | Progress value (0.0 - 1.0) | +| `Bar` | PatchStyle / String | Bar appearance | +| `BarTexturePath` | UI Path (String) | Bar texture | +| `EffectTexturePath` | UI Path (String) | Effect overlay texture | +| `EffectWidth` | Integer | Effect width | +| `EffectHeight` | Integer | Effect height | +| `EffectOffset` | Integer | Effect offset | +| `Alignment` | ProgressBarAlignment | Bar alignment | +| `Direction` | ProgressBarDirection | Fill direction | + +**Example:** +```ui +ProgressBar #HealthBar { + Anchor: (Width: 200, Height: 20); + Value: 0.75; + Direction: LeftToRight; + Background: PatchStyle(Color: #333333); + Bar: PatchStyle(Color: #22cc22); +} +``` + +--- + +### ItemGrid + +**Scrollable item grid** - Accepts children: No + +| Property | Type | Description | +|----------|------|-------------| +| `Slots` | ItemGridSlot[] | Grid slot data | +| `ItemStacks` | ClientItemStack[] | Simple item stacks | +| `SlotsPerRow` | Integer | Items per row | +| `ShowScrollbar` | Boolean | Show scrollbar | +| `Style` | ItemGridStyle | Grid style | +| `ScrollbarStyle` | ScrollbarStyle | Scrollbar style | +| `RenderItemQualityBackground` | Boolean | Show quality backgrounds | +| `InfoDisplay` | ItemGridInfoDisplayMode | Info display mode | +| `AdjacentInfoPaneGridWidth` | Integer | Info pane width | +| `AreItemsDraggable` | Boolean | Enable drag and drop | +| `InventorySectionId` | Integer | Inventory section ID | +| `AllowMaxStackDraggableItems` | Boolean | Allow full stack dragging | +| `DisplayItemQuantity` | Boolean | Show stack quantities | +| `KeepScrollPosition` | Boolean | Preserve scroll position | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `SlotDoubleClicking` | Slot double-clicked | +| `DragCancelled` | Drag operation cancelled | +| `SlotMouseEntered` | Mouse entered slot | +| `SlotMouseExited` | Mouse left slot | + +**Example:** +```ui +ItemGrid #InventoryGrid { + FlexWeight: 1; + SlotsPerRow: 8; + ShowScrollbar: true; + AreItemsDraggable: false; + RenderItemQualityBackground: true; + Style: $Common.@DefaultItemGridStyle; +} +``` + +--- + +### Sprite + +**Animated spritesheet image** - Accepts children: No + +| Property | Type | Description | +|----------|------|-------------| +| `TexturePath` | UI Path (String) | Spritesheet texture | +| `Frame` | SpriteFrame | Spritesheet layout info | +| `FramesPerSecond` | Integer | Animation speed | +| `IsPlaying` | Boolean | Animation playing state | +| `AutoPlay` | Boolean | Auto-play on mount | +| `Angle` | Float | Rotation in degrees | +| `RepeatCount` | Integer | Repeat count (0 = infinite) | + +**Example:** +```ui +Sprite #LoadingSpinner { + Anchor: (Width: 32, Height: 32); + TexturePath: "spinner@2x.png"; + FramesPerSecond: 12; + AutoPlay: true; + RepeatCount: 0; +} +``` + +--- + +### TabNavigation + +**Tab bar** - Accepts children: Yes + +| Property | Type | Description | +|----------|------|-------------| +| `Tabs` | Tab[] | Tab definitions | +| `SelectedTab` | String | Currently selected tab | +| `AllowUnselection` | Boolean | Allow no selection | +| `Style` | TabNavigationStyle | Tab bar style | + +**Event Callbacks:** + +| Event | Description | +|-------|-------------| +| `SelectedTabChanged` | Tab selection changed | + +**Example:** +```ui +TabNavigation #MainTabs { + Anchor: (Height: 40); + SelectedTab: "inventory"; + Style: $Common.@DefaultTabNavigationStyle; +} +``` + +--- + +### All Elements List + +| Element | Children | Description | +|---------|----------|-------------| +| **Group** | Yes | Container/layout element | +| **Label** | No | Text display | +| **Button** | Yes | Clickable button | +| **TextField** | No | Single-line text input | +| **MultilineTextField** | No | Multi-line text input | +| **NumberField** | No | Numeric input | +| **Slider** | No | Range slider | +| **FloatSlider** | No | Float range slider | +| **CheckBox** | No | Boolean toggle | +| **DropdownBox** | Yes | Dropdown selection | +| **ProgressBar** | No | Progress indicator | +| **CircularProgressBar** | No | Circular progress | +| **ItemGrid** | No | Item slot grid | +| **ItemSlot** | No | Single item slot | +| **ItemIcon** | No | Item icon display | +| **ItemSlotButton** | No | Clickable item slot | +| **Sprite** | No | Animated spritesheet | +| **TabNavigation** | Yes | Tab bar | +| **TabButton** | Yes | Individual tab | +| **TextButton** | Yes | Text-labeled button | +| **ToggleButton** | Yes | Toggle button | +| **ActionButton** | Yes | Action button | +| **BackButton** | Yes | Back navigation | +| **Panel** | Yes | Styled panel | +| **SceneBlur** | No | Background blur effect | +| **TimerLabel** | No | Timer display | +| **HotkeyLabel** | No | Keybind display | +| **ReorderableList** | Yes | Drag-sortable list | +| **ReorderableListGrip** | No | Drag handle | +| **ColorPicker** | No | Color selection | +| **ColorPickerDropdownBox** | Yes | Color dropdown | +| **ColorOptionGrid** | No | Color option grid | +| **CodeEditor** | No | Code editing | +| **AssetImage** | No | Asset image display | +| **CharacterPreviewComponent** | No | Character preview | +| **ItemPreviewComponent** | No | Item preview | +| **BlockSelector** | No | Block selection | +| **DynamicPane** | Yes | Dynamic content pane | +| **DynamicPaneContainer** | Yes | Dynamic pane container | +| **LabeledCheckBox** | No | CheckBox with label | +| **SliderNumberField** | No | Slider with number field | +| **FloatSliderNumberField** | No | Float slider with number field | +| **CompactTextField** | No | Compact text input | +| **DropdownEntry** | No | Dropdown list item | +| **MenuItem** | Yes | Menu item | +| **CheckBoxContainer** | Yes | CheckBox container | + +--- + +## Property Types + +Property types define complex value structures used as element properties. + +### Anchor + +Defines element positioning and sizing within its container. + +| Property | Type | Description | +|----------|------|-------------| +| `Left` | Integer | Distance from container's left edge | +| `Right` | Integer | Distance from container's right edge | +| `Top` | Integer | Distance from container's top edge | +| `Bottom` | Integer | Distance from container's bottom edge | +| `Width` | Integer | Fixed width in pixels | +| `Height` | Integer | Fixed height in pixels | +| `MinWidth` | Integer | Minimum width constraint | +| `MaxWidth` | Integer | Maximum width constraint | +| `MinHeight` | Integer | Minimum height constraint | +| `MaxHeight` | Integer | Maximum height constraint | + +**Shorthand:** +- `Full` - All sides (Left, Top, Right, Bottom) +- `Horizontal` - Left and Right +- `Vertical` - Top and Bottom + +**Example:** +```ui +Anchor: (Width: 200, Height: 40); // Fixed size +Anchor: (Full: 10); // 10px margin all sides +Anchor: (Top: 0, Bottom: 0, Left: 20, Width: 300); // Mixed +``` + +--- + +### Padding + +Inner spacing around content. + +| Property | Type | Description | +|----------|------|-------------| +| `Left` | Integer | Left padding | +| `Right` | Integer | Right padding | +| `Top` | Integer | Top padding | +| `Bottom` | Integer | Bottom padding | + +**Shorthand:** +- `Full` - All sides +- `Horizontal` - Left and Right +- `Vertical` - Top and Bottom + +**Example:** +```ui +Padding: (Full: 16); +Padding: (Horizontal: 20, Vertical: 10); +``` + +--- + +### PatchStyle + +Nine-slice scalable backgrounds. + +| Property | Type | Description | +|----------|------|-------------| +| `Color` | Color | Background color | +| `TexturePath` | UI Path (String) | Background texture | +| `Border` | Integer | Border thickness (all sides) | +| `HorizontalBorder` | Integer | Horizontal border thickness | +| `VerticalBorder` | Integer | Vertical border thickness | +| `Area` | Padding | Content area | +| `Anchor` | Anchor | Positioning | + +**Example:** +```ui +Background: PatchStyle(Color: #1a1a2eF0, Border: 4); +Background: PatchStyle(TexturePath: "frame@2x.png", Border: 8); +``` + +--- + +### LabelStyle + +Text styling properties. + +| Property | Type | Description | +|----------|------|-------------| +| `FontName` | String | Font name (Default, Secondary, Mono) | +| `FontSize` | Float | Font size | +| `TextColor` | Color | Text color | +| `OutlineColor` | Color | Text outline color | +| `LetterSpacing` | Float | Space between letters | +| `HorizontalAlignment` | LabelAlignment | Horizontal alignment | +| `VerticalAlignment` | LabelAlignment | Vertical alignment | +| `Alignment` | LabelAlignment | Combined alignment | +| `Wrap` | Boolean | Enable text wrapping | +| `RenderUppercase` | Boolean | Render as uppercase | +| `RenderBold` | Boolean | Render bold | +| `RenderItalics` | Boolean | Render italic | +| `RenderUnderlined` | Boolean | Render underlined | + +**Example:** +```ui +Style: LabelStyle( + FontSize: 16, + TextColor: #ffffff, + RenderBold: true, + HorizontalAlignment: Center +); +``` + +--- + +### LabelSpan + +Rich text span for formatted text. + +| Property | Type | Description | +|----------|------|-------------| +| `Text` | String | Span text content | +| `Color` | Color | Text color | +| `OutlineColor` | Color | Outline color | +| `IsBold` | Boolean | Bold text | +| `IsItalics` | Boolean | Italic text | +| `IsUppercase` | Boolean | Uppercase text | +| `IsUnderlined` | Boolean | Underlined text | +| `IsMonospace` | Boolean | Monospace font | +| `Link` | String | Clickable link URL | +| `Params` | Dictionary | Additional parameters | + +**Example:** +```ui +TextSpans: [ + (Text: "Normal "), + (Text: "Bold ", IsBold: true), + (Text: "Colored", Color: #ff6600) +]; +``` + +--- + +### ItemGridSlot + +Item grid slot data. + +| Property | Type | Description | +|----------|------|-------------| +| `ItemStack` | ClientItemStack | Item to display | +| `Background` | PatchStyle / String | Slot background | +| `Overlay` | PatchStyle / String | Overlay on top of item | +| `Icon` | PatchStyle / String | Icon overlay | +| `ExtraOverlays` | List<PatchStyle> | Additional overlays | +| `Name` | String | Custom name override | +| `Description` | String | Custom description | +| `InventorySlotIndex` | Integer | Inventory slot index | +| `IsItemIncompatible` | Boolean | Mark as incompatible | +| `IsActivatable` | Boolean | Can be activated | +| `IsItemUncraftable` | Boolean | Mark as uncraftable | +| `SkipItemQualityBackground` | Boolean | Skip quality background | + +--- + +### All Property Types List + +| Type | Description | +|------|-------------| +| **Anchor** | Element positioning/sizing | +| **Padding** | Inner spacing | +| **PatchStyle** | Nine-slice backgrounds | +| **LabelStyle** | Text styling | +| **LabelSpan** | Rich text span | +| **ItemGridSlot** | Item grid slot | +| **ItemGridStyle** | Item grid styling | +| **ClientItemStack** | Item stack data | +| **ButtonStyle** | Button styling | +| **ButtonStyleState** | Button state styling | +| **ButtonSounds** | Button sound effects | +| **CheckBoxStyle** | CheckBox styling | +| **CheckBoxStyleState** | CheckBox state styling | +| **SliderStyle** | Slider styling | +| **InputFieldStyle** | Text input styling | +| **InputFieldDecorationStyle** | Input decoration | +| **InputFieldDecorationStyleState** | Input state decoration | +| **InputFieldIcon** | Input field icon | +| **InputFieldButtonStyle** | Input button styling | +| **DropdownBoxStyle** | Dropdown styling | +| **DropdownBoxSounds** | Dropdown sounds | +| **DropdownBoxSearchInputStyle** | Dropdown search styling | +| **ScrollbarStyle** | Scrollbar styling | +| **TabStyle** | Tab styling | +| **TabStyleState** | Tab state styling | +| **TabNavigationStyle** | Tab bar styling | +| **Tab** | Tab definition | +| **TextButtonStyle** | TextButton styling | +| **TextButtonStyleState** | TextButton state styling | +| **ToggleButtonStyle** | ToggleButton styling | +| **ToggleButtonStyleState** | ToggleButton state styling | +| **LabeledCheckBoxStyle** | LabeledCheckBox styling | +| **LabeledCheckBoxStyleState** | LabeledCheckBox state styling | +| **PopupStyle** | Popup styling | +| **TextTooltipStyle** | Tooltip styling | +| **SoundStyle** | Sound effect | +| **SpriteFrame** | Spritesheet frame info | +| **NumberFieldFormat** | Number formatting | +| **ColorPickerStyle** | ColorPicker styling | +| **ColorPickerDropdownBoxStyle** | Color dropdown styling | +| **ColorPickerDropdownBoxStateBackground** | Color dropdown state | +| **ColorOptionGridStyle** | Color grid styling | +| **BlockSelectorStyle** | Block selector styling | +| **SubMenuItemStyle** | Submenu styling | +| **SubMenuItemStyleState** | Submenu state styling | + +--- + +## Enums + +Enums define valid values for specific properties. + +### LayoutMode + +How a container arranges its children. + +| Value | Description | +|-------|-------------| +| `Full` | Children fill parent; positioned via Anchor | +| `Left` | Left-to-right, aligned left | +| `Center` | Left-to-right, centered horizontally | +| `Right` | Left-to-right, aligned right | +| `Top` | Top-to-bottom, aligned top | +| `Middle` | Top-to-bottom, centered vertically | +| `Bottom` | Top-to-bottom, aligned bottom | +| `CenterMiddle` | Left-to-right, centered both axes | +| `MiddleCenter` | Top-to-bottom, centered both axes | +| `LeftScrolling` | Like Left with scrolling | +| `RightScrolling` | Like Right with scrolling | +| `TopScrolling` | Like Top with scrolling | +| `BottomScrolling` | Like Bottom with scrolling | +| `LeftCenterWrap` | Left-to-right, wrap to next row, centered | + +--- + +### LabelAlignment + +Text alignment. + +| Value | Description | +|-------|-------------| +| `Left` | Align left | +| `Center` | Align center | +| `Right` | Align right | + +--- + +### ProgressBarDirection + +Progress bar fill direction. + +| Value | Description | +|-------|-------------| +| `LeftToRight` | Fill from left to right | +| `RightToLeft` | Fill from right to left | +| `TopToBottom` | Fill from top to bottom | +| `BottomToTop` | Fill from bottom to top | + +--- + +### ProgressBarAlignment + +Progress bar alignment within container. + +| Value | Description | +|-------|-------------| +| `Start` | Align to start | +| `Center` | Align to center | +| `End` | Align to end | + +--- + +### TimerDirection + +Timer count direction. + +| Value | Description | +|-------|-------------| +| `Up` | Count up | +| `Down` | Count down | + +--- + +### All Enums List + +| Enum | Values | +|------|--------| +| **LayoutMode** | Full, Left, Center, Right, Top, Middle, Bottom, CenterMiddle, MiddleCenter, LeftScrolling, RightScrolling, TopScrolling, BottomScrolling, LeftCenterWrap | +| **LabelAlignment** | Left, Center, Right | +| **ProgressBarDirection** | LeftToRight, RightToLeft, TopToBottom, BottomToTop | +| **ProgressBarAlignment** | Start, Center, End | +| **TimerDirection** | Up, Down | +| **ResizeType** | None, Horizontal, Vertical, Both | +| **TooltipAlignment** | (alignment values) | +| **ActionButtonAlignment** | (alignment values) | +| **DropdownBoxAlign** | (alignment values) | +| **InputFieldButtonSide** | (side values) | +| **InputFieldIconSide** | (side values) | +| **ItemGridInfoDisplayMode** | (display modes) | +| **ColorFormat** | (color formats) | +| **CodeEditorLanguage** | (language values) | +| **MouseWheelScrollBehaviourType** | (behavior types) | + +--- + +## Source + +Full type documentation: https://hytalemodding.dev/en/docs/official-documentation/custom-ui/type-documentation diff --git a/skills/hytale-world-gen/SKILL.md b/skills/hytale-world-gen/SKILL.md new file mode 100644 index 0000000..43fcdde --- /dev/null +++ b/skills/hytale-world-gen/SKILL.md @@ -0,0 +1,497 @@ +--- +name: hytale-world-gen +description: Documents Hytale's procedural world generation systems for plugin development. Covers the Java API (Zones, Biomes, Caves) and the data-driven node/JSON system (Density, Curves, Patterns, Material Providers, Props, Scanners, Positions, Assignments, Directionality, Vector Providers, Block Masks). Use when creating custom zones, biomes, caves, terrain, world generation features, density fields, asset packs, or working with the Hytale Node Editor. Triggers - world gen, world generation, zone, biome, cave, ZonePatternGenerator, BiomePatternGenerator, CaveGenerator, CaveType, Zone, CustomBiome, TileBiome, ZoneDiscoveryConfig, ZoneGeneratorResult, CaveBiomeMaskFlags, terrain, procedural generation, border transition, biome mask, density, noise, simplex, curves, patterns, material provider, props, prefab, scanner, positions, assignments, directionality, vector provider, block mask, asset pack, HytaleGenerator, node editor. +--- + +# Hytale World Generation System + +Comprehensive reference for Hytale's procedural world generation systems used to create environments, biomes, and structures dynamically as players explore the world. + +> **Related skills:** For ECS fundamentals, see `hytale-ecs`. For persistent data/Codec patterns, see `hytale-persistent-data`. For entity effects, see `hytale-entity-effects`. + +## Quick Reference + +| Task | Approach | +|------|----------| +| Get zone at position | `zoneGen.generate(seed, x, z)` returns `ZoneGeneratorResult` | +| Get zone from result | `zoneResult.getZone()` returns `Zone` | +| Get biome in zone | `zone.biomePatternGenerator().generateBiomeAt(zoneResult, seed, x, z)` | +| Get cave generator | `zone.caveGenerator()` returns `CaveGenerator` (nullable) | +| Get cave types | `caveGen.getCaveTypes()` returns `CaveType[]` | +| Check biome cave mask | `CaveBiomeMaskFlags.canGenerate(flags)` | +| Get border distance | `zoneResult.getBorderDistance()` | +| Fade near border | `customBiome.getFadeContainer().getMaskFactor(result)` | +| Create zone discovery | `new ZoneDiscoveryConfig(...)` | +| Create biome pattern | `new BiomePatternGenerator(points, tileBiomes, customBiomes)` | +| Create cave config | `new CaveGenerator(caveTypes)` | +| Create custom zone | `new Zone(id, name, discovery, caveGen, biomeGen, prefabs)` | + +--- + +## Overview + +Hytale's world generation is organized into three interconnected systems: + +``` +World Generation +├── Zones — Large-scale regions defining overall world structure +├── Biomes — Terrain characteristics and environment within zones +└── Caves — Underground structures and networks within zones +``` + +These systems are hierarchical: **Zones** contain **Biomes**, and both Zones and Biomes influence **Cave** generation. They work together to produce smooth transitions and coherent regional themes. + +--- + +## Zones + +Zones are the largest-scale division in world generation. Each zone defines its own biome patterns, cave configurations, and unique structures (prefabs). + +### Zone Lookup and Generation + +```java +// Get zone at a world position +ZonePatternGenerator zoneGen = /* from world generator */; +ZoneGeneratorResult zoneResult = zoneGen.generate(seed, x, z); +Zone zone = zoneResult.getZone(); +``` + +### ZoneGeneratorResult + +The result object provides: + +| Method | Returns | Description | +|--------|---------|-------------| +| `getZone()` | `Zone` | The zone at the queried position | +| `getBorderDistance()` | `double` | Distance to the nearest zone border | + +### Zone Class + +A `Zone` encapsulates all generation data for a region: + +| Property | Type | Description | +|----------|------|-------------| +| ID | `int` | Unique numeric identifier | +| Name | `String` | Internal name (e.g., `"new_custom_zone"`) | +| Discovery | `ZoneDiscoveryConfig` | Player notification on zone entry | +| Cave Generator | `CaveGenerator` | Cave configuration (nullable) | +| Biome Pattern | `BiomePatternGenerator` | Biome layout within the zone | +| Unique Prefabs | — | Unique structures placed in the zone | + +### Creating a Custom Zone + +```java +// 1. Configure zone discovery (player notification on entry) +ZoneDiscoveryConfig discovery = new ZoneDiscoveryConfig( + true, // Show notification + "Custom Zone", // Display name + "zone.forest.discover", // Sound event + "icons/forest.png", // Icon + true, // Major zone + 5.0f, 2.0f, 1.5f // Duration, fade in, fade out +); + +// 2. Create biome pattern +IPointGenerator biomePoints; +IWeightedMap tileBiomes; +CustomBiome[] customBiomes; + +BiomePatternGenerator biomeGen = new BiomePatternGenerator( + biomePoints, + tileBiomes, + customBiomes +); + +// 3. Create cave configuration +CaveType[] caveTypes; // cave definitions +CaveGenerator caveGen = new CaveGenerator(caveTypes); + +// 4. Assemble the zone +Zone customZone = new Zone( + 100, // Unique ID + "new_custom_zone", // Internal name + discovery, // Discovery config + caveGen, // Cave generator + biomeGen, // Biome pattern + uniquePrefabs // Unique structures +); +``` + +### ZoneDiscoveryConfig + +Controls what the player sees when entering a zone: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `showNotification` | `boolean` | Whether to display the zone entry notification | +| `displayName` | `String` | Name shown to the player | +| `soundEvent` | `String` | Sound played on zone entry | +| `icon` | `String` | Icon path for the notification | +| `majorZone` | `boolean` | Whether this is a major zone (affects display) | +| `duration` | `float` | How long the notification is visible (seconds) | +| `fadeIn` | `float` | Fade-in duration (seconds) | +| `fadeOut` | `float` | Fade-out duration (seconds) | + +--- + +## Biomes + +Biomes define terrain characteristics, vegetation, and environmental properties within a zone. Each zone has its own `BiomePatternGenerator` that determines biome layout. + +### Biome Lookup + +```java +// Get the biome at a specific position within a zone +Zone zone = zoneResult.getZone(); +BiomePatternGenerator biomeGen = zone.biomePatternGenerator(); +Biome biome = biomeGen.generateBiomeAt(zoneResult, seed, x, z); +``` + +### BiomePatternGenerator + +Constructs the biome layout from three inputs: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `biomePoints` | `IPointGenerator` | Point distribution controlling biome placement | +| `tileBiomes` | `IWeightedMap` | Weighted map of tile-level biome data | +| `customBiomes` | `CustomBiome[]` | Custom biome definitions with fade/transition settings | + +### Biome Properties + +Each `Biome` has at minimum: + +| Method | Returns | Description | +|--------|---------|-------------| +| `getId()` | `int` | Unique biome identifier | + +### CustomBiome Fading + +Custom biomes support smooth transitions near zone borders via a fade container: + +```java +CustomBiome customBiome = /* ... */; +FadeContainer fade = customBiome.getFadeContainer(); +double maskFadeSum = fade.getMaskFadeSum(); +double factor = fade.getMaskFactor(zoneResult); +``` + +--- + +## Caves + +Caves are underground structures generated within zones. Each zone optionally has a `CaveGenerator` with one or more `CaveType` definitions. + +### Cave Lookup + +```java +Zone zone = /* ... */; +CaveGenerator caveGen = zone.caveGenerator(); +if (caveGen != null) { + // This zone has caves + CaveType[] types = caveGen.getCaveTypes(); +} +``` + +### CaveType and Biome Masks + +Cave types use biome masks to control where caves can generate: + +```java +// Cave type with biome restrictions +Int2FlagsCondition biomeMask = caveType.getBiomeMask(); +int biomeId = biome.getId(); +int flags = biomeMask.eval(biomeId); + +// Check if cave can generate in this biome +if (CaveBiomeMaskFlags.canGenerate(flags)) { + // Generate cave +} +``` + +| Class | Purpose | +|-------|---------| +| `CaveGenerator` | Holds cave type definitions for a zone | +| `CaveType` | Individual cave definition with shape, biome mask, etc. | +| `Int2FlagsCondition` | Evaluates biome ID → flags for cave placement | +| `CaveBiomeMaskFlags` | Utility to interpret biome mask flags (e.g., `canGenerate`) | + +--- + +## System Integration + +### Zone → Biome Integration + +Each zone defines its own biome pattern. The biome generator requires the zone result (for border distance and zone context): + +```java +Zone zone = zoneGen.generate(seed, x, z).getZone(); +BiomePatternGenerator biomeGen = zone.biomePatternGenerator(); +Biome biome = biomeGen.generateBiomeAt(zoneResult, seed, x, z); +``` + +### Zone → Cave Integration + +Each zone defines its own cave patterns. Not all zones have caves: + +```java +Zone zone = /* ... */; +CaveGenerator caveGen = zone.caveGenerator(); +if (caveGen != null) { + CaveType[] types = caveGen.getCaveTypes(); +} +``` + +### Biome → Cave Integration + +Caves use biome masks to restrict generation to specific biomes: + +```java +Int2FlagsCondition biomeMask = caveType.getBiomeMask(); +int biomeId = biome.getId(); +int flags = biomeMask.eval(biomeId); + +if (CaveBiomeMaskFlags.canGenerate(flags)) { + // Cave can generate in this biome +} +``` + +### Border Transitions + +All systems respect zone boundaries. Custom biomes fade near borders for smooth transitions: + +```java +ZoneGeneratorResult result = zoneGen.generate(seed, x, z); +double borderDistance = result.getBorderDistance(); + +// Fade custom biomes near borders +if (borderDistance < customBiome.getFadeContainer().getMaskFadeSum()) { + double factor = customBiome.getFadeContainer().getMaskFactor(result); + // Apply fading based on factor (0.0 = fully faded, 1.0 = full strength) +} +``` + +--- + +## Key Classes Summary + +| Class | Package Area | Purpose | +|-------|-------------|---------| +| `ZonePatternGenerator` | worldgen | Generates zones at world positions | +| `ZoneGeneratorResult` | worldgen | Result of zone lookup (zone + border distance) | +| `Zone` | worldgen | Zone definition (biomes, caves, prefabs, discovery) | +| `ZoneDiscoveryConfig` | worldgen | Player notification on zone entry | +| `BiomePatternGenerator` | worldgen | Biome layout within a zone | +| `Biome` | worldgen | Biome data (ID, properties) | +| `CustomBiome` | worldgen | Custom biome with fade/transition support | +| `TileBiome` | worldgen | Tile-level biome data | +| `IPointGenerator` | worldgen | Point distribution for biome placement | +| `IWeightedMap` | worldgen | Weighted map for biome selection | +| `CaveGenerator` | worldgen | Cave configuration for a zone | +| `CaveType` | worldgen | Individual cave type definition | +| `Int2FlagsCondition` | worldgen | Biome mask evaluator for caves | +| `CaveBiomeMaskFlags` | worldgen | Flag utility for cave biome masks | +| `FadeContainer` | worldgen | Border fade/transition controller | + +--- + +## Common Patterns + +### Full World Position → Zone + Biome + Cave Pipeline + +```java +// Complete lookup pipeline +ZonePatternGenerator zoneGen = /* from world generator */; +long seed = /* world seed */; +int x = /* world x */; +int z = /* world z */; + +// 1. Determine zone +ZoneGeneratorResult zoneResult = zoneGen.generate(seed, x, z); +Zone zone = zoneResult.getZone(); + +// 2. Determine biome within zone +BiomePatternGenerator biomeGen = zone.biomePatternGenerator(); +Biome biome = biomeGen.generateBiomeAt(zoneResult, seed, x, z); + +// 3. Check for caves +CaveGenerator caveGen = zone.caveGenerator(); +if (caveGen != null) { + for (CaveType caveType : caveGen.getCaveTypes()) { + Int2FlagsCondition biomeMask = caveType.getBiomeMask(); + int flags = biomeMask.eval(biome.getId()); + if (CaveBiomeMaskFlags.canGenerate(flags)) { + // This cave type can generate here + } + } +} + +// 4. Handle border transitions +double borderDistance = zoneResult.getBorderDistance(); +// Use borderDistance for custom blending/fading logic +``` + +### Zone Entry Notification Setup + +```java +// Minimal zone discovery config +ZoneDiscoveryConfig discovery = new ZoneDiscoveryConfig( + true, // Show notification + "Enchanted Forest", // Display name + "zone.forest.discover", // Sound event + "icons/forest.png", // Icon + true, // Major zone + 5.0f, 2.0f, 1.5f // Duration, fade in, fade out +); +``` + +--- + +--- + +## Data-Driven World Generation (Node/JSON System) + +Beyond the Java plugin API, Hytale's world generation is primarily **data-driven** through JSON asset files and an in-game **Node Editor**. Biomes, terrain shapes, materials, and content placement are all defined declaratively. + +> **Detailed references** for every node type are in the `references/` subdirectory of this skill. + +### Asset Directory Structure + +``` +Server/HytaleGenerator/ +├── WorldStructure/ # Generator files defining which biomes spawn together +├── Biomes/ # Biome assets with content configurations +├── Density/ # Reusable density assets referenced by other generation assets +└── Assignments/ # Reusable prop assignment assets referenced by biomes +``` + +### World Instance Configuration + +Instances are separate worlds that players can join. Each instance has an `Instance.bson` that specifies which generator to use: + +```json +{ + "WorldGen": { + "Type": "HytaleGenerator", + "WorldStructure": "Basic", + "playerSpawn": { + "X": 123, "Y": 480, "Z": 10000, + "Pitch": 0, "Yaw": 0, "Roll": 0 + } + } +} +``` + +Instance configs are stored at `Server/Instances/`. Create custom instances by duplicating the basic config and changing `WorldStructure` to your asset name. + +### Previewing World Generation + +| Command | Purpose | +|---------|---------| +| `/viewport --radius 5` | Live-reload area around player as you edit | +| `/instances spawn ` | Create a new world with latest changes | +| Fly south | New chunks generate with latest changes | + +### Biome Asset Structure + +Every biome has 5 root components: + +| Component | Purpose | +|-----------|---------| +| **Terrain** | Density function nodes defining the physical terrain shape | +| **Material Provider** | Logic nodes determining which block types make up terrain | +| **Props** | Object nodes placing prefabs, trees, POIs, grass, etc. | +| **Environment Provider** | Logic nodes determining weather, NPC spawns, sounds per coordinate | +| **Tint Provider** | Function nodes determining color codes for grasses, soils, etc. | + +Each biome is self-contained — it controls everything within its boundaries except Base Heights. + +### Node System Overview + +World generation uses a node-based system of composable JSON assets. Each node type has parameters and inputs: + +| Node Category | Purpose | Key Types | +|---------------|---------|-----------| +| **Density** | 3D decimal value fields for terrain shape | SimplexNoise2D/3D, PositionsCellNoise, Distance, Ellipsoid, Cube, Cylinder, Sum, Multiplier, Mix, CurveMapper, Scale, Rotator, Warp nodes | +| **Curves** | f(x)=y mappings for value transformation | Manual, DistanceExponential, DistanceS, Ceiling/Floor, SmoothClamp, Clamp, Inverter, Min/Max | +| **Patterns** | Validate world positions by material/structure | BlockType, BlockSet, Floor, Ceiling, Wall, Surface, Gap, Cuboid, And/Or/Not, FieldFunction | +| **Material Providers** | Determine block types at positions | Constant, Solidity, Queue, SimpleHorizontal, Striped, Weighted, FieldFunction, SpaceAndDepth (with Layers and Conditions) | +| **Positions Provider** | Define infinite 3D position fields | Mesh2D, Mesh3D, List, FieldFunction, Occurrence, BaseHeight, Cache | +| **Scanners** | Scan local world areas for valid positions | Origin, ColumnLinear, ColumnRandom, Area | +| **Props** | Localized content placed in the world | Box, Density, Prefab, Column, Cluster, Union, Weighted, Queue, PondFiller | +| **Assignments** | Assign props to positions | Constant, FieldFunction, Sandwich, Weighted | +| **Directionality** | Determine prop placement direction | Static, Random, Pattern-based | +| **Vector Provider** | Define 3D vectors procedurally | Constant, DensityGradient, Cache | +| **Block Mask** | Control which materials can replace others | DontPlace, DontReplace, Advanced rules | + +### Core Concept: Density Fields + +Terrain generation is built on **density fields** — maps of decimal values (typically -1 to 1) defining terrain shape: +- **Positive values** → solid terrain +- **Negative values** → empty space (air) +- Density is calculated from the sum of all nodes: `f(x) = z + y` + +Common terrain formula: **Simplex 2D noise** + **Y-Curve** (height gradient) = basic terrain shape. + +Function nodes manipulate density fields: Absolute (ridged shapes), Normalization (rescaling range), Scale (stretch/contract), Rotator (orientation), Warp (distortion), and many more. + +### Core Concept: Props Pipeline + +Props place procedural content (trees, prefabs, grass) using three connected systems: + +``` +Positions → Scanner → Pattern → Prop Placement +``` + +1. **Positions** provide candidate locations (e.g., Mesh2D grid, BaseHeight offsets) +2. **Scanner** searches around each position (e.g., ColumnLinear scans Y range) +3. **Pattern** validates positions (e.g., Floor pattern checks for solid ground below) +4. **Prop** places content at validated positions (e.g., Prefab with Directionality) + +### Asset Packs + +Asset Packs override or add assets to the base game. Required for world gen customization: + +```json +{ + "Group": "My Group", + "Name": "Pack Example", + "Version": "1.0.0", + "Description": "An Example Asset Pack", + "Authors": [{"Name": "Me", "Email": "", "Url": ""}], + "Dependencies": {}, + "DisabledByDefault": false, + "IncludesAssetPack": false, + "SubPlugins": [] +} +``` + +- Stored in `C:\Users\\AppData\Roaming\Hytale\UserData\Mods` +- Per-world packs stored in `UserData\Saves\\mods` +- Asset Packs exist entirely on the server — only the host needs them +- Assets inside packs override base game assets + +### Import/Export System + +Most node types support `Imported` and `Exported` nodes for reusability: +- **Exported**: Makes a node tree available by name, with optional `SingleInstance` for shared caching +- **Imported**: References an exported asset by name +- This enables modular, reusable density fields, curves, patterns, etc. + +--- + +## Notes + +- The `worldgen` module is a built-in Hytale module. Plugins interact with it through the public API classes listed above. +- Zone IDs must be unique across the world generation configuration. +- `CaveGenerator` is nullable — not all zones have caves. +- Biome masks use a flags-based system (`Int2FlagsCondition`) for efficient cave-biome filtering. +- Border transitions use `FadeContainer` with `getMaskFadeSum()` and `getMaskFactor()` for smooth blending between zones. +- The `--validate-world-gen` server launch flag can be used to validate world generation assets. +- World gen examples can be found in `HytaleGenerator/Biomes/Examples/`. +- The Hytale Node Editor is accessible in-game from the Content Creation menu (Tab key). +- Performance tip: In Multiplier nodes, order inputs with cheapest mask first — the node skips remaining inputs after a 0 value. +- Performance tip: Use Cache/Cache2D nodes on expensive density lookups that are queried multiple times. + +> **Source:** [hytalemodding.dev — World Generation System](https://hytalemodding.dev/en/docs/guides/plugin/world-gen) +> **Source:** [HytaleModding/site — Official World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) diff --git a/skills/hytale-world-gen/references/biome-editing-guide.md b/skills/hytale-world-gen/references/biome-editing-guide.md new file mode 100644 index 0000000..933312d --- /dev/null +++ b/skills/hytale-world-gen/references/biome-editing-guide.md @@ -0,0 +1,107 @@ +# Biome Editing Guide + +> **Source:** [Official Hytale World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) + +## Asset Locations + +World generation assets are in the `HytaleGenerator` directory within server assets: + +``` +Server/HytaleGenerator/ +├── WorldStructure/ # Generator files defining which biomes spawn together +├── Biomes/ # Biome assets with content configurations +├── Density/ # Reusable density assets referenced by other assets +└── Assignments/ # Prop assignment assets referenced by biome assets +``` + +## World Instances + +Instances are separate worlds players can join via `/instances`. Each has an `Instance.bson` in `Server/Instances/`: + +```json +{ + "WorldGen": { + "Type": "HytaleGenerator", + "WorldStructure": "Basic", + "playerSpawn": { + "X": 123, "Y": 480, "Z": 10000, + "Pitch": 0, "Yaw": 0, "Roll": 0 + } + } +} +``` + +Create custom instances by duplicating the basic Instance config and changing `WorldStructure` to your custom asset name. + +## Editing Biomes + +The Hytale Node Editor is used for editing world generation assets. Access it in-game from the Content Creation menu (press Tab). + +Open biomes via the file menu, navigating to the Biomes directory: +``` +Server/HytaleGenerator/Biomes/Basic.json +``` + +> **Requirement:** You must have an Asset Pack with a Biome. + +## Biome Asset Structure + +Every biome has a root Biome node that splits into 5 components: + +### 1. Terrain +Mathematical function nodes that calculate the physical shape of the biome's terrain. Uses Density nodes. + +### 2. Material Provider +Logical nodes determining what block types make up the biome's terrain. Uses Material Provider nodes with conditions and layers. + +### 3. Props +Object function nodes that add objects such as prefabs to the terrain. Configure content like trees, POIs, grass, etc. Uses Props, Scanners, Patterns, Positions, and Assignments. + +### 4. Environment Provider +Logical nodes determining the environment asset at a given coordinate within the biome. Controls weather, NPC spawns, and ambient sounds. + +### 5. Tint Provider +Logical function nodes that determine a color code used by certain material types (typically grasses and soils). + +> **Note:** Each biome is self-contained — it controls everything within its boundaries. The only exception is Base Heights. + +## Previewing Changes + +| Method | Command/Action | Description | +|--------|----------------|-------------| +| Viewport | `/viewport --radius 5` | Live-reloads an area around the player as you edit | +| New Instance | `/instances spawn ` | Creates a new world showing latest changes | +| Fly South | — | New chunks generate with the latest changes | + +## Asset Packs + +An Asset Pack is a zip or folder with a `manifest.json` inside. All Asset Packs require a manifest, and assets must be in the correct folder structure. + +```json +{ + "Group": "My Group", + "Name": "Pack Example", + "Version": "1.0.0", + "Description": "An Example Asset Pack", + "Authors": [{"Name": "Me", "Email": "", "Url": ""}], + "Website": "", + "Dependencies": {}, + "OptionalDependencies": {}, + "LoadBefore": {}, + "DisabledByDefault": false, + "IncludesAssetPack": false, + "SubPlugins": [] +} +``` + +### Storage Locations + +| Location | Path | +|----------|------| +| Global mods | `C:\Users\\AppData\Roaming\Hytale\UserData\Mods` | +| Per-world | `C:\Users\\AppData\Roaming\Hytale\UserData\Saves\\mods` | + +- Asset Packs created in-game with the Asset Editor are stored per-world. Copy to the global Mods folder for use in other worlds. +- Asset Packs exist entirely on the server — only the server/host needs them installed. +- Assets inside packs override base game assets. You can also add new assets. +- Can be downloaded from hosting sites such as CurseForge. diff --git a/skills/hytale-world-gen/references/curve-types.md b/skills/hytale-world-gen/references/curve-types.md new file mode 100644 index 0000000..f1845fa --- /dev/null +++ b/skills/hytale-world-gen/references/curve-types.md @@ -0,0 +1,87 @@ +# Curve Types Reference + +> **Source:** [Official Hytale World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) + +Curves map decimal values to other decimal values (f(x) = y). Used throughout world generation for value transformation. + +## Base Curves + +### Manual +Plot points connected with straight lines. Function is constant before first point and after last point. +- **Parameters:** List of {x, y} points + +### DistanceExponential +As input approaches Range, outputs 0.0. At input 0.0, outputs 1.0. Shape controlled by Exponent. +- **Parameters:** `Exponent` (affects shape), `Range` (value after which output is constant 0.0) + +### DistanceS +Combines two DistanceExponent curves for an S-shaped profile. At input 0.0, outputs 1.0. At Range, outputs 0.0. +- **Parameters:** `ExponentA` (float > 0 — first half shape), `ExponentB` (float > 0 — second half shape), `Range` (float > 0), `Transition` (optional 0-1, default 1.0 — lower = more sudden transition), `TransitionSmooth` (optional 0-1, default 1.0) + +## Limit/Clamp Curves + +### Ceiling +Caps the output of a child curve. +- **Parameters:** `Ceiling` (decimal — max output), `Curve` (curve slot) + +### Floor +Sets a minimum for the output of a child curve. +- **Parameters:** `Floor` (decimal — min output), `Curve` (curve slot) + +### SmoothCeiling +Caps output with smooth approach to the limit. +- **Parameters:** `Ceiling` (decimal), `Range` (decimal ≥ 0 — smoothing amount, start: ¼ of child range), `Curve` (curve slot) + +### SmoothFloor +Sets smooth minimum for output. +- **Parameters:** `Floor` (decimal), `Range` (decimal ≥ 0), `Curve` (curve slot) + +### SmoothClamp +Limits output within walls with smooth transitions. +- **Parameters:** `WallA` (decimal), `WallB` (decimal), `Range` (decimal ≥ 0), `Curve` (curve slot) + +### Clamp +Hard clamp between two walls. +- **Parameters:** `WallA` (decimal), `WallB` (decimal), `Curve` (curve slot) + +## Combination Curves + +### SmoothMax +Smoothed maximum of two curves. +- **Parameters:** `Range` (decimal ≥ 0 — start: ¼ of child range), `CurveA` (curve slot), `CurveB` (curve slot) + +### SmoothMin +Smoothed minimum of two curves. +- **Parameters:** `Range` (decimal ≥ 0), `CurveA` (curve slot), `CurveB` (curve slot) + +### Max +Maximum value of all child curves. +- **Parameters:** `Curves` (list of curve slots) + +### Min +Minimum value of all child curves. +- **Parameters:** `Curves` (list of curve slots) + +### Multiplier +Product of all child curves. +- **Parameters:** `Curves` (list of curve slots) + +### Sum +Sum of all child curves. +- **Parameters:** `Curves` (list of curve slots) + +## Logic/Transform Curves + +### Inverter +Positive → negative and vice versa. +- **Parameters:** `Curve` (curve slot) + +### Not +Logical NOT: when child outputs 1 → outputs 0; when 0 → outputs 1; scaled in between. +- **Parameters:** `Curve` (curve slot) + +## Import + +### Imported +Imports an exported Curve. +- **Parameters:** `Name` (string — the exported Density asset name) diff --git a/skills/hytale-world-gen/references/density-nodes.md b/skills/hytale-world-gen/references/density-nodes.md new file mode 100644 index 0000000..9caac6e --- /dev/null +++ b/skills/hytale-world-gen/references/density-nodes.md @@ -0,0 +1,260 @@ +# Density Nodes Reference + +> **Source:** [Official Hytale World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) + +Density nodes define 3D decimal value fields used to shape terrain. They are composed in trees where outputs feed into inputs. + +## Noise Generators + +### Constant +Outputs a constant value. +- **Parameters:** `Value` (decimal) +- **Inputs:** 0 + +### SimplexNoise2D +Outputs [-1, 1] from a 2D simplex noise field varying on x/z plane. Automatically caches per x/z column. +- **Parameters:** `Lacunarity` (float, start: 2.0), `Persistence` (float, start: 0.5), `Scale` (float, start: 50), `Octaves` (int, start: 4), `Seed` (string) +- **Inputs:** 0 + +### SimplexNoise3D +Outputs [-1, 1] from a 3D simplex noise field varying on x/y/z space. +- **Parameters:** `Lacunarity` (float, start: 2.0), `Persistence` (float, start: 0.5), `ScaleXZ` (float, start: 50), `ScaleY` (float, start: 50), `Octaves` (int, start: 4), `Seed` (string) +- **Inputs:** 0 + +### PositionsCellNoise +Produces a 2D/3D density field based on distance from a Positions field. Supports advanced cell noise with configurable ReturnTypes. +- **Parameters:** `Positions` (positions slot), `ReturnType` (see below), `DistanceFunction` (Euclidean or Manhattan), `MaxDistance` (float — set slightly over half the distance between position points) +- **Inputs:** 0 + +**ReturnTypes:** +| Type | Description | Extra Parameters | +|------|-------------|------------------| +| CellValue | Constant density inside each cell sampled from a Density field | `Density` (slot), `DefaultValue` (float) | +| Density | Cell populated with a Density field picked from Delimiters by ChoiceDensity | `Density` (slot), `DefaultValue` (float) | +| Curve | Value defined by a Curve asset based on distance from Positions | `Curve` (curve slot) | +| Distance | Traditional CellNoise distance return | None | +| Distance2 | Traditional CellNoise distance2 return | None | +| Distance2Add | Sum of two nearest distances | None | +| Distance2Sub | Difference of two nearest distances | None | +| Distance2Mul | Product of two nearest distances | None | +| Distance2Div | Division of two nearest distances | None | + +**DistanceFunction:** `Euclidean` or `Manhattan` (no parameters) + +## Shape Generators + +### Distance +Value based on distance from origin {0,0,0}. +- **Parameters:** `Curve` (curve slot — maps distance to density) +- **Inputs:** 0 + +### Ellipsoid +Deformed sphere with scale, rotation, and spin. +- **Parameters:** `Curve` (curve slot), `Scale` (3D vector slot), `X/Y/Z` (floats), `Spin` (degrees around Y axis) +- **Inputs:** 0 +- **Deformation order:** 1. Scale → 2. Align Y axis → 3. Spin + +### Cube +Density based on distance from origin axis. +- **Parameters:** `Curve` (curve slot) +- **Inputs:** 0 + +### Cuboid +Deformed cube with scale, rotation, and spin. +- **Parameters:** `Curve` (curve slot), `Scale` (3D vector slot), `X/Y/Z` (floats), `Spin` (degrees), `NewYAxis` (Point3D slot) +- **Inputs:** 0 +- **Deformation order:** 1. Scale → 2. Align Y axis → 3. Spin + +### Cylinder +Cylindrical shape with axial and radial curves. +- **Parameters:** `AxialCurve` (curve — density along Y axis), `RadialCurve` (curve — density by distance from Y axis), `Spin` (degrees), `NewYAxis` (Point3D slot) +- **Inputs:** 0 + +### Axis +Density based on distance from a line through origin. +- **Parameters:** `Axis` (3D vector slot), `X/Y/Z` (floats), `Curve` (curve slot), `IsAnchored` (boolean — uses closest anchor) +- **Inputs:** 0 + +### Plane +Density based on distance from a user-defined plane through origin. +- **Parameters:** `PlaneNormal` (3D vector slot), `X/Y/Z` (floats), `Curve` (curve slot) +- **Inputs:** 0 + +### Shell +Density regions of a shell around origin based on direction and distance. +- **Parameters:** `Axis` (3D vector slot), `X/Y/Z` (floats), `Mirror` (boolean), `AngleCurve` (curve slot), `DistanceCurve` (curve slot) +- **Inputs:** 0 + +## Math Operations + +### Sum +Output is the sum of all inputs. +- **Inputs:** [0, ∞) + +### Multiplier +Output is the product of all inputs. **Performance tip:** Skips remaining inputs after a 0 value — order with cheapest mask first for optimization (up to ~40% improvement). +- **Inputs:** [0, ∞) + +### Max / Min +Greatest/smallest value of all inputs (0 if no inputs). +- **Inputs:** [0, ∞) + +### SmoothMax / SmoothMin +Smoothed maximum/minimum between two inputs. +- **Parameters:** `Range` (float, start: 0.2 — greater = more smoothing) +- **Inputs:** 2 + +### Mix +Mixes two inputs controlled by a third gauge input. +- Gauge ≤ 0.0 → only Density A; Gauge ≥ 1.0 → only Density B; between = proportional mix +- **Inputs:** 3 (Density A, Density B, Gauge) + +### MultiMix +Mixes multiple inputs with a Gauge (last input). Keys pin inputs to gauge values. +- **Inputs:** unlimited (last is Gauge) + +### Clamp +Ensures output is within [WallA, WallB]. +- **Parameters:** `WallA` (float), `WallB` (float) +- **Inputs:** 1 + +### SmoothClamp +Like Clamp but with smooth transition at limits. +- **Parameters:** `WallA` (float), `WallB` (float), `Range` (float — larger = smoother) +- **Inputs:** 1 + +### Abs +Absolute value of input. +- **Inputs:** 1 + +### Sqrt +Square root (modified for negative inputs to always return useful values). +- **Inputs:** 1 + +### Inverter +Input × -1. +- **Inputs:** 1 + +### Pow +Input raised to exponent power (modified for negative inputs). +- **Parameters:** `Exponent` (float, start: 2) +- **Inputs:** 1 + +### CurveMapper +Maps input through a Curve. +- **Parameters:** `Curve` (curve slot) +- **Inputs:** 1 + +### Normalizer +Rescales input from one range to another. +- **Parameters:** `FromMin`, `FromMax`, `ToMin`, `ToMax` (all floats) +- **Inputs:** 1 + +## Coordinate & Cache + +### XValue / YValue / ZValue +Outputs the local X/Y/Z coordinate. +- **Inputs:** 0 + +### XOverride / YOverride / ZOverride +Overrides the X/Y/Z coordinate that the input sees. +- **Inputs:** 1 + +### Cache +Caches input for current coordinates. +- **Parameters:** `Capacity` (int, safe value: 3) +- **Inputs:** 1 + +## Transform Nodes + +### Scale +Stretches/contracts the input density field per axis. Values > 1 stretch, < 1 contract, < 0 flip. +- **Parameters:** `X`, `Y`, `Z` (floats) +- **Inputs:** 1 +- **Tip:** Combine with Rotator nodes for non-orthogonal scaling + +### Rotator +Aligns input field's Y axis to a new axis and spins. +- **Parameters:** `NewYAxis` (3D vector), `X/Y/Z` (floats), `SpinAngle` (degrees) +- **Inputs:** 1 + +### Slider +Slides input field in a direction. +- **Parameters:** `SlideX`, `SlideY`, `SlideZ` (floats) +- **Inputs:** 1 + +### Anchor +Anchors the child field's origin to the contextual Anchor (e.g., cell center from PositionsCellNoise Density ReturnType). +- **Parameters:** `Reverse` (boolean — if true, moves origin back to world origin) +- **Inputs:** 1 + +## Warp Nodes + +### GradientWarp +Warps first input based on gradient of second input. Relatively expensive — minimize use space. Incorporates Cache2D. +- **Parameters:** `SampleRange` (float, recommend 1), `WarpFactor` (float — larger = more warping), `2D` (boolean — uses internal Cache2D), `YFor2D` (float, default 0) +- **Inputs:** 2 (field to warp, warping field) + +### FastGradientWarp +Faster implementation using internal simplex noise generator. +- **Parameters:** `WarpScale` (float), `WarpLacunarity` (float), `WarpPersistence` (float), `WarpOctaves` (int), `WarpFactor` (float — max warp distance) +- **Inputs:** 1 + +### VectorWarp +Warps input along a provided vector. Warp amount = second input value × WarpFactor. +- **Parameters:** `WarpFactor` (float), `WarpVector` (vector slot), `X/Y/Z` (floats) +- **Inputs:** 2 (field to warp, warping intensity field) + +### PositionsPinch +Pinches or expands density field around Positions. PinchCurve defines effect shape, MaxDistance defines range. +- **Parameters:** `Positions` (slot), `PinchCurve` (curve slot), `MaxDistance` (float), `NormalizeDistance` (boolean, default true), `HorizontalPinch` (boolean, default false), `PositionsMaxY`, `PositionsMinY` (floats) +- **Inputs:** 1 + +### PositionsTwist +Twists density field around Positions. TwistCurve output is in degrees (360 = full rotation). +- **Parameters:** `Positions` (slot), `TwistCurve` (curve slot), `TwistAxis` (vector slot), `X/Y/Z` (floats), `MaxDistance` (float), `NormalizeDistance` (boolean, default true) +- **Inputs:** 1 + +## Context Nodes + +### Angle +Angle in degrees between two vectors. +- **Parameters:** `Vector` (3D vector), `VectorProvider` (procedural vector) +- **Inputs:** 0 + +### DistanceToBiomeEdge +Outputs distance to nearest biome edge in blocks. +- **Inputs:** 0 + +### Terrain +Outputs interpolated terrain Density. **Only for MaterialProvider nodes — not for Terrain Density nodes.** +- **Inputs:** 0 + +### BaseHeight +References a BaseHeight from WorldStructure. +- **Parameters:** `BaseHeightName` (string), `Distance` (boolean — false: raw Y coordinate, true: distance from BaseHeight) +- **Inputs:** 0 + +## Branching Nodes + +### Switch +Switches between Density branches based on contextual SwitchState string. +- **Parameters:** `SwitchCases` (list of case slots with `CaseState` string + `Density` slot) +- **Inputs:** 1 + +### SwitchState +Sets the contextual SwitchState for downstream branches. +- **Parameters:** `SwitchState` (string) +- **Inputs:** 1 + +## Import/Export + +### Imported +Imports an exported Density asset. +- **Parameters:** `Name` (string) + +### Exported +Exports a Density field. `SingleInstance` shares the exported tree across all importers (useful for cache optimization). +- **Parameters:** `SingleInstance` (boolean), `Density` (slot) +- **Inputs:** 1 +- **Note:** Experimental feature — may cause unexpected behaviors if misused. diff --git a/skills/hytale-world-gen/references/prop-placement-nodes.md b/skills/hytale-world-gen/references/prop-placement-nodes.md new file mode 100644 index 0000000..135c347 --- /dev/null +++ b/skills/hytale-world-gen/references/prop-placement-nodes.md @@ -0,0 +1,230 @@ +# Prop Placement Nodes Reference (Props, Positions, Scanners, Assignments, Directionality) + +> **Source:** [Official Hytale World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) + +These node categories work together in a pipeline to place content in the world: + +``` +Positions (where to look) → Scanner (how to search) → Pattern (what's valid) → Prop (what to place) + ↑ + Assignments (which prop) + Directionality (facing) +``` + +--- + +## Props + +Localized content that reads and writes to the world. + +### Box +Testing/debugging prop that generates a box. Uses Pattern and Scanner. +- **Parameters:** `Range` (3D int vector: X, Y, Z distances), `BoxBlockType` (block type), `Pattern` (Pattern slot), `Scanner` (Scanner slot) + +### Density +Generates a prop from a Density field and MaterialProvider. Uses Pattern, Scanner, and BlockMask. +- **Parameters:** `Range` (3D int vector), `PlacementMask` (BlockMask slot), `Pattern` (Pattern slot), `Scanner` (Scanner slot), `Density` (Density slot — shape), `Material` (MaterialProvider slot — block types) + +### Prefab +Places a Prefab structure in a suitable spot. Uses Directionality, Scanner, and BlockMask. +- **Parameters:** + - `WeightedPrefabPaths` — list of weighted entries: `Path` (folder/file), `Weight` (float), `LegacyPath` (boolean — true: `Server/World/Default/Prefabs/`, false: `Server/WorldgenAlt/Prefabs/`) + - `Directionality` (slot — controls facing) + - `Scanner` (Scanner slot) + - `BlockMask` (BlockMask slot) + - `MoldingDirection` (optional: "UP", "DOWN", or "NONE" default) + - `MoldingChildren` (optional boolean — children also mold) + - `MoldingScanner` (optional Scanner slot — use LinearScanner with Local=true, ResultCap=1) + - `MoldingPattern` (optional Pattern slot — finds surface to mold to) + - `LoadEntities` (optional boolean — load entities from prefabs) + +### Column +Prop contained within a single column. +- **Parameters:** `ColumnBlocks` (list of `BlockType` string + `Y` int relative to origin), `Directionality` (slot), `Scanner` (Scanner slot), `BlockMask` (optional) + +### Cluster +Places a cluster of Column Props around an origin. Density of placement varies by distance from origin. +- **Parameters:** `Range` (int — distance from origin, start: 10), `DistanceCurve` (curve — density by distance), `Seed` (string), `Pattern` (optional), `Scanner` (optional), `WeightedProps` (list of `Weight` float + `ColumnProp` slot) +- **Important:** Column Props in a Cluster must use Scanner/Pattern that operate within a single column. + +### Union +Places all props in the list at the same position. +- **Parameters:** `Props` (list of prop slots) + +### Offset +Offsets child Prop's position. +- **Parameters:** `Offset` (3D int vector), `Prop` (Prop slot) + +### Weighted +Picks which Prop to place based on seed and weights. +- **Parameters:** `Entries` (list of `Weight` float + `Prop` slot), `Seed` (string) + +### Queue +Places first Prop in queue that can be placed (based on its Scanner/Pattern config). +- **Parameters:** `Queue` (ordered list of Prop slots, first = highest priority) + +### PondFiller +Fills terrain depressions with material (e.g., water). **Performance note:** impact depends on bounding box size — optimize per use case. +- **Parameters:** `BoundingMin` (3D point — lowest corner), `BoundingMax` (3D point — greatest corner), `BarrierBlockSet` (BlockSet slot — solid terrain types), `FillMaterial` (MaterialProvider slot — fill blocks), `Pattern` (Pattern slot), `Scanner` (Scanner slot) + +### Imported +Imports an exported Prop. +- **Parameters:** `Name` (string) + +--- + +## Positions Provider + +Defines infinite 3D position fields used by Props and Density nodes. + +### Mesh2D +Generates a mesh of random points on a 2D plane. +- **Parameters:** `PointGenerator` (slot), `PointsY` (vertical Y position) +- **PointGenerator Parameters:** `Jitter` (0-0.5, start: 0.2), `ScaleX`/`ScaleY`/`ScaleZ` (positive decimals), `Seed` (string) + +### Mesh3D +Generates a mesh of random points in 3D space. +- **Parameters:** `PointGenerator` (slot) + +### List +Static list of positions in world coordinates. +- **Parameters:** `Positions` (list of X, Y, Z integers) + +### Anchor +Anchors child Positions to contextual Anchor point. +- **Parameters:** `Reverse` (boolean — reverses back to world origin) + +### Sphere +Masks out positions farther than Range from origin. +- **Parameters:** `Range` (decimal — max distance) + +### FieldFunction +Masks positions using a Density field and delimiters. +- **Parameters:** `FieldFunction` (Density slot), `Delimiters` (list with `Min`/`Max`), `Positions` (slot) + +### Occurrence +Discards positions based on Density field probability. +- Density ≤ 0 → 0% chance kept; Density ≥ 1 → 100% kept; between = proportional +- **Parameters:** `FieldFunction` (Density slot), `Seed` (string), `Positions` (slot) + +### Offset +Offsets positions by a vector. +- **Parameters:** `OffsetX`, `OffsetY`, `OffsetZ` (decimals), `Positions` (slot) + +### BaseHeight +Vertically offsets positions by BaseHeight amount. Positions outside the Y region are discarded. +- **Parameters:** `BaseHeightName` (string), `MaxYRead` (decimal — exclusive), `MinYRead` (decimal — inclusive) + +### Union +Combines all positions into one field. +- **Parameters:** `Positions` (list of slots) + +### SimpleHorizontal +Keeps only positions within a Y range. +- **Parameters:** `RangeY` (range), `Positions` (slot) + +### Cache +Caches output in 3D sections. Useful for expensive Positions trees queried multiple times. +- **Parameters:** `SectionsSize` (int > 0, start: 32), `CacheSize` (int ≥ 0, start: 50; 0 = no cache), `Positions` (slot) +- **Tip:** Place close to the root of Positions tree. + +### Imported +Imports an exported PositionProvider. +- **Parameters:** `Name` (string) + +--- + +## Scanners + +Scan local parts of the world for valid positions matching a Pattern. + +### Origin +Only scans the origin position. +- No parameters. + +### ColumnLinear +Scans a column of blocks linearly (top-down or bottom-up). +- **Parameters:** `MaxY` (int — upper exclusive), `MinY` (int — lower inclusive), `RelativeToPosition` (boolean — relative to scan origin Y instead of world Y:0), `BaseHeightName` (optional string — overrides RelativeToPosition), `TopDownOrder` (boolean), `ResultCap` (positive int — max valid results) + +### ColumnRandom +Scans a column randomly with two strategies. +- **Parameters:** `MaxY` (int), `MinY` (int), `Strategy` ("DART_THROW" — random samples, good for many valid positions; or "PICK_VALID" — finds all valid then picks, good for few valid positions), `Seed` (string), `ResultCap` (int), `RelativeToPosition` (boolean), `BaseHeightName` (optional string) + +### Area +Scans an expanding area around the origin using a child Scanner. +- **Parameters:** `ScanRange` (int ≥ 0 — distance in blocks, start: 0), `ScanShape` ("CIRCLE" or "SQUARE"), `ResultCap` (int), `ChildScanner` (Scanner slot — applied to each column) + +### Imported +Imports an exported Scanner. +- **Parameters:** `Name` (string) + +--- + +## Assignments + +Assign Props to each position in a Positions field. + +### Constant +Assigns one Prop to all positions. +- **Parameters:** `Prop` (Prop slot) + +### FieldFunction +Selects which props to assign based on a Density field and value delimiters. +- **Parameters:** `FieldFunction` (Density slot), `Delimiters` (list with `Min`/`Max` + `Assignments` slot) + +### Sandwich +Selects props based on vertical (world Y) position delimiters. +- **Parameters:** `Delimiters` (list with `MinY`/`MaxY` + `Assignments` slot) + +### Weighted +Picks props randomly based on weights and seed. +- **Parameters:** `Seed` (string), `SkipChance` (0-1 — chance to skip), `WeightedAssignments` (list of weighted Assignments slots) + +### Imported +Imports an exported Assignments. +- **Parameters:** `Name` (string) + +--- + +## Directionality + +Determines the direction (rotation) to place a Prop. + +### Static +Fixed direction. +- **Parameters:** `Rotation` (int: 0, 90, 180, or 270; default 0), `Pattern` (Pattern slot — locates position, doesn't affect direction) + +### Random +Random direction based on seed. +- **Parameters:** `Seed` (string), `Pattern` (Pattern slot) + +### Pattern (Directionality Type) +Direction based on environment. Links directions to Pattern assets for each cardinal direction. +- **Parameters:** `InitialDirection` (string: "N", "S", "E", "W" — prop's original facing), `NorthPattern`/`SouthPattern`/`EastPattern`/`WestPattern` (Pattern slots), `Seed` (string — for tie-breaking) + +--- + +## Vector Provider + +Defines 3D decimal vectors procedurally. + +### Constant +Fixed vector. +- **Parameters:** `Vector` (3D decimal vector) + +### DensityGradient +Gradient of a Density field — shows direction/rate of density change. +- **Parameters:** `SampleDistance` (positive decimal, optimal: 1.0), `Density` (Density slot) + +### Cache +Caches vector per position. Only use if downstream VectorProvider is expensive and queried multiple times. +- **Parameters:** `SampleDistance` (decimal), `Density` (Density slot) + +### Exported +Exports a VectorProvider. SingleInstance shares across all importers. +- **Parameters:** `SingleInstance` (boolean), `VectorProvider` (slot) +- **Note:** Experimental feature. + +### Imported +Imports an exported VectorProvider. +- **Parameters:** `Name` (string) diff --git a/skills/hytale-world-gen/references/terrain-nodes.md b/skills/hytale-world-gen/references/terrain-nodes.md new file mode 100644 index 0000000..018c321 --- /dev/null +++ b/skills/hytale-world-gen/references/terrain-nodes.md @@ -0,0 +1,146 @@ +# Terrain Nodes Reference (Patterns, Material Providers, Block Mask) + +> **Source:** [Official Hytale World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) + +--- + +## Patterns + +Patterns validate world locations based on material composition and other criteria. Used by Props and Scanners to find valid positions. + +### BlockType +Checks against the block's material. +- **Parameters:** `Material` (block material name) + +### BlockSet +Checks if block material belongs to a BlockSet. +- **Parameters:** `BlockSet` (slot) + +### Offset +Offsets the child Pattern by a vector. +- **Parameters:** `Pattern` (Pattern slot), `Offset` (3D integer vector) + +### Floor +Checks for a floor below the position. +- **Parameters:** `Floor` (Pattern slot — validates block under origin), `Origin` (Pattern slot — validates at origin) + +### Ceiling +Checks for a ceiling above the position. +- **Parameters:** `Ceiling` (Pattern slot — validates block above origin), `Origin` (Pattern slot — validates at origin) + +### Wall +Checks for a wall next to the position. Supports N/S/E/W directions. +- **Parameters:** `Wall` (Pattern slot — validates adjacent block), `Origin` (Pattern slot), `Directions` (list: "N", "S", "E", "W"), `RequireAllDirections` (boolean) + +### Surface +Validates a transition from one set of materials to another (e.g., soil floor in air). +- **Parameters:** `Surface` (Pattern slot), `Medium` (Pattern slot), `SurfaceRadius` (decimal ≥ 0), `MediumRadius` (decimal ≥ 0), `SurfaceGap` (int ≥ 0), `MediumGap` (int ≥ 0), `Facings` (list: "N","S","E","W","U","D"), `RequireAllFacings` (boolean) + +### Gap +Validates a space between two anchors (e.g., for bridge placement). +- **Parameters:** `GapSize` (decimal ≥ 0), `AnchorSize` (decimal ≥ 0), `AnchorRoughness` (decimal ≥ 0, start: 1), `DepthDown` (int ≥ 0), `DepthUp` (int ≥ 0), `Angles` (list of degrees, 0=Z axis, 90=X axis), `GapPattern` (Pattern slot), `AnchorPattern` (Pattern slot) + +### Cuboid +Defines a cuboid region relative to origin. Validates if all inner positions pass SubPattern. +- **Parameters:** `Min` (3D point — inclusive min), `Max` (3D point — inclusive max), `SubPattern` (Pattern slot) + +### And / Or / Not +Logical operators combining patterns. +- **And Parameters:** `Patterns` (list of Pattern slots) — all must validate +- **Or Parameters:** `Patterns` (list of Pattern slots) — at least one must validate +- **Not Parameters:** `Pattern` (single Pattern slot) — validates where nested does not + +### FieldFunction +Validates if Density field value at position is within delimiters. **Performance note:** expensive — place last in Pattern hierarchy. +- **Parameters:** `FieldFunction` (Density slot), `Delimiters` (list with `Min`/`Max` decimal bounds) + +### Imported +Imports an exported Pattern. +- **Parameters:** `Name` (string) + +--- + +## Material Providers + +Determine which block type to place at each position. + +### Constant +One constant block type. +- **Parameters:** `BlockType` (string, e.g. "Rock_Stone") + +### Solidity +Splits into Solid and Empty terrain blocks. +- **Parameters:** `Solid` (Material Provider slot), `Empty` (Material Provider slot) + +### Queue +Priority queue of Material Providers. First slot to provide a block wins. +- **Parameters:** `Queue` (list of Material Provider slots, top = highest priority) + +### SimpleHorizontal +Applies child Material Provider on a vertical range. If BaseHeight provided, Y values are relative to it. +- **Parameters:** `TopY` (int), `Top BaseHeight` (string, optional), `BottomY` (int), `Bottom BaseHeight` (string, optional), `Material` (Material Provider slot) + +### Striped +Applies Material Provider on horizontal stripes of varying thickness. +- **Parameters:** `Stripes` (list with `TopY`/`BottomY` ints, inclusive), `Material` (Material Provider slot) + +### Weighted +Picks Material Provider from weighted list. +- **Parameters:** `Seed` (string), `SkipChance` (0-1 — % of blocks skipped), `WeightedMaterials` (list with `Weight`/`Material` entries) + +### FieldFunction +Selects 3D region using a noise function and value delimiters. +- **Parameters:** `FieldFunction` (Density slot — shares seeds with terrain density), `Delimiters` (list with `From`/`To` floats + `Material` slot; higher in list = higher priority) + +### SpaceAndDepth +Places layers of blocks on floor or ceiling surfaces. Layers pile like a cake into the surface depth. +- **Parameters:** `LayerContext` (string: "DEPTH_INTO_FLOOR" or "DEPTH_INTO_CEILING"), `MaxExpectedDepth` (int — sum of max thicknesses of all layers), `Condition` (optional Condition slot), `Layers` (list of Layer objects) + +#### Condition Types +Conditions check environment validity before applying material. + +| Type | Parameters | Description | +|------|-----------|-------------| +| EqualsCondition | `ContextToCheck`, `Value` (int) | Context equals value | +| GreaterThanCondition | `ContextToCheck`, `Threshold` (int) | Context > threshold | +| SmallerThanCondition | `ContextToCheck`, `Threshold` (int) | Context < threshold | +| AndCondition | `Conditions` (list) | All must validate | +| OrCondition | `Conditions` (list) | Any must validate | +| NotCondition | `Condition` (single slot) | Inverts result | +| AlwaysTrueCondition | None | Always validates | + +Context values: `SPACE_ABOVE_FLOOR`, `SPACE_BELOW_CEILING` + +#### Layer Types + +| Type | Parameters | Description | +|------|-----------|-------------| +| ConstantThickness | `Material` (slot), `Thickness` (int ≥ 0) | Same thickness everywhere | +| RangeThickness | `Material` (slot), `RangeMin`/`RangeMax` (int ≥ 0), `Seed` | Random thickness in range | +| WeightedThickness | `Material` (slot), `PossibleThicknesses` (weighted list), `Seed` | Weighted random thickness | +| NoiseThickness | `Material` (slot), `ThicknessFunctionXZ` (Density slot — 2D) | Thickness from noise function | + +### Imported +Imports an exported MaterialProvider. +- **Parameters:** `Name` (string) + +--- + +## Block Mask + +Controls which materials can replace which other materials when placing content. + +- **Source Material:** The material being placed +- **Destination Material:** The material already in the world + +### Rules + +| Rule | Description | +|------|-------------| +| **DontPlace** | BlockSet — source materials that will not be placed | +| **DontReplace** | BlockSet — destination materials that cannot be replaced (default) | +| **Advanced** | List of override rules with `Source` BlockSet and `CanReplace` BlockSet | + +Advanced rules override DontReplace for specific source/destination combinations. + +**Parameters:** `DontPlace` (BlockSet slot), `DontReplace` (BlockSet slot), `Advanced` (list of rules) diff --git a/skills/hytale-world-gen/references/world-gen-concepts.md b/skills/hytale-world-gen/references/world-gen-concepts.md new file mode 100644 index 0000000..666c2da --- /dev/null +++ b/skills/hytale-world-gen/references/world-gen-concepts.md @@ -0,0 +1,62 @@ +# World Generation Concepts + +> **Source:** [Official Hytale World Generation Documentation](https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen) + +## Generative Noise + +Hytale's terrain generation is founded on **density fields** — maps of decimal values used to define terrain shape. Density fields can be built from sources of procedural noise (such as Simplex and Cellular), contextual data, and processing nodes. + +## Density and Solidity + +Density generates a range of values, typically between 1 and -1: +- **Positive values** = solid terrain +- **Negative values** = empty space (air) +- Density is calculated from the sum: `f(x) = z + y` + +## Function Nodes + +All density fields can be manipulated with function nodes provided in the node editor. Important transformations: + +### Absolute +Makes all negative values positive, creating ridged shapes on noise fields. + +### Normalization +Moves the 0 value to a new position and stretches the field to a new range. Used to control what fraction of the field is solid vs empty. + +> **Note:** The Hytale Node Editor does not currently support noise visualization, but examples can be found in `HytaleGenerator/Biomes/Examples/`. + +## Noise-Based Terrain + +A basic terrain shape is created by combining: +1. **Simplex 2D** — 2-dimensional generative noise field +2. **Y-Curve** — 2-dimensional curve drawn between a height differential + +This formula creates most of the terrain in Hytale. + +## Material Providers + +Materials use logical function nodes to determine which block type is placed at each location. These run on: +- **Solid portions** of the terrain field (terrain blocks) +- **Negative portions** (e.g., filling in a water level) + +Key concepts: +- **Solidity** splits into Solid and Empty material providers +- **Queue** creates a priority list of materials (top = highest priority) + +> **Note:** The Hytale Node Editor sets node priority based on position in the editor, shown as a number in the top right corner. + +## Props + +Props generate content in specific limited regions. Hytale provides different Prop types for procedurally placing content in the world. + +The prop placement pipeline: + +``` +Positions → Scanner → Pattern → Content Placement +``` + +| Component | Purpose | +|-----------|---------| +| **Positions** | Provides candidate locations for scanning | +| **Scanner** | Defines an area/column around each position to search | +| **Pattern** | Validates positions based on world material composition | diff --git a/skills/update-hytale-skills/SKILL.md b/skills/update-hytale-skills/SKILL.md new file mode 100644 index 0000000..8bc60cf --- /dev/null +++ b/skills/update-hytale-skills/SKILL.md @@ -0,0 +1,584 @@ +--- +name: update-hytale-skills +description: Updates existing hytale-* skills and detects new documentation pages on the HytaleModding site. Checks the GitHub source repo (HytaleModding/site) for content changes, fetches updated MDX source files, and reconciles skill content. Also cross-references decompiled server source for skills without upstream doc URLs. Use after server updates, periodically, or when new modding docs are published. Triggers - update skills, refresh skills, sync docs, check for skill updates, new documentation, skill maintenance, hytalemodding site changes, update modding docs. +--- + +# Update Hytale Skills + +Procedural skill for keeping all `hytale-*` skills in `.github/skills/` synchronized with upstream documentation from the [HytaleModding site](https://hytalemodding.dev/en/docs) and the decompiled server source in `lib/`. + +> **Related skills:** `update-server-lib` handles updating `lib/` with the latest Hytale server JAR and decompiled source. Run that skill **first** if a new server version is available, then run this skill to update knowledge skills. + +--- + +## When to Use This Skill + +- After running `update-server-lib` (new server version deployed) +- When the HytaleModding documentation site has been updated +- Periodically (e.g., weekly) to catch community doc improvements +- When you notice a skill's code examples or API references are outdated +- When a new guide or doc page appears on hytalemodding.dev that has no matching skill + +--- + +## Source Repository + +All documentation lives in the **HytaleModding/site** GitHub repository: + +- **Repo:** `https://github.com/HytaleModding/site` +- **Content root:** `content/docs/en/` +- **Raw content base URL:** `https://raw.githubusercontent.com/HytaleModding/site/main/content/docs/en/` + +### Content Directory Structure + +``` +content/docs/en/ +├── guides/ +│ ├── ecs/ # ECS guides +│ │ ├── entity-component-system.mdx +│ │ ├── hytale-ecs-theory.mdx +│ │ ├── systems.mdx +│ │ ├── example-ecs-plugin.mdx +│ │ └── block-components.mdx +│ ├── java-basics/ # Java tutorial series (skip — not skill material) +│ │ ├── 00-introduction.mdx ... 13-inheritance.mdx +│ ├── plugin/ # Plugin guides (most skills map here) +│ │ ├── chat-formatting.mdx +│ │ ├── creating-commands.mdx +│ │ ├── creating-configuration-file.mdx +│ │ ├── creating-events.mdx +│ │ ├── customizing-camera-controls.mdx +│ │ ├── customizing-hotbar-actions.mdx +│ │ ├── instances.mdx +│ │ ├── inventory-management.mdx +│ │ ├── item-interaction.mdx +│ │ ├── item-registry.mdx +│ │ ├── listening-to-packets.mdx +│ │ ├── logging.mdx +│ │ ├── permission-management.mdx +│ │ ├── player-death-event.mdx +│ │ ├── player-input-guide.mdx +│ │ ├── player-stats.mdx +│ │ ├── playing-sounds.mdx +│ │ ├── send-notifications.mdx +│ │ ├── spawning-entities.mdx +│ │ ├── spawning-npcs.mdx +│ │ ├── store-persistent-data.mdx +│ │ ├── teleporting-players.mdx +│ │ ├── text-hologram.mdx +│ │ ├── ui.mdx +│ │ ├── world-gen.mdx +│ │ ├── build-and-test.mdx +│ │ ├── browsing-serverjar.mdx +│ │ ├── client-inputs-reference.mdx +│ │ └── creating-block.mdx +│ └── prefabs.mdx +├── official-documentation/ +│ ├── custom-ui/ # Official UI docs +│ │ ├── common-styling.mdx +│ │ ├── layout.mdx +│ │ ├── markup.mdx +│ │ └── type-documentation/ # (subdirectory with type docs) +│ ├── npc/ # Official NPC template docs (12 chapters) +│ │ ├── 1-know-your-enemy.mdx +│ │ ├── 2-getting-started-with-templates.mdx +│ │ ├── ...through 12-appendix.mdx +│ └── worldgen/ # Official world gen docs +│ ├── pack-tutorial/ +│ ├── technical-hytale-generator/ +│ └── worldgen-tutorial/ +├── server/ # Server reference docs +│ ├── entities.mdx +│ ├── events.mdx +│ └── sounds.mdx +└── index.mdx +``` + +--- + +## Skill-to-Source Mapping + +The table below maps each `hytale-*` skill to its upstream documentation source(s). Use this to know exactly which files to check for updates. + +### Skills with Upstream Doc URLs + +| Skill | GitHub Source Path(s) | Live URL(s) | +|-------|----------------------|-------------| +| `hytale-camera-controls` | `guides/plugin/customizing-camera-controls.mdx` | [customizing-camera-controls](https://hytalemodding.dev/en/docs/guides/plugin/customizing-camera-controls) | +| `hytale-chat-formatting` | `guides/plugin/chat-formatting.mdx` | [chat-formatting](https://hytalemodding.dev/en/docs/guides/plugin/chat-formatting) | +| `hytale-config-files` | `guides/plugin/creating-configuration-file.mdx` | [creating-configuration-file](https://hytalemodding.dev/en/docs/guides/plugin/creating-configuration-file) | +| `hytale-ecs` | `guides/ecs/entity-component-system.mdx`, `guides/ecs/hytale-ecs-theory.mdx`, `guides/ecs/systems.mdx`, `guides/ecs/example-ecs-plugin.mdx`, `guides/ecs/block-components.mdx` | [entity-component-system](https://hytalemodding.dev/en/docs/guides/ecs/entity-component-system), [hytale-ecs-theory](https://hytalemodding.dev/en/docs/guides/ecs/hytale-ecs-theory), [systems](https://hytalemodding.dev/en/docs/guides/ecs/systems), [example-ecs-plugin](https://hytalemodding.dev/en/docs/guides/ecs/example-ecs-plugin), [block-components](https://hytalemodding.dev/en/docs/guides/ecs/block-components) | +| `hytale-events` | `guides/plugin/creating-events.mdx`, `server/events.mdx` | [creating-events](https://hytalemodding.dev/en/docs/guides/plugin/creating-events), [events](https://hytalemodding.dev/en/docs/server/events) | +| `hytale-hotbar-actions` | `guides/plugin/customizing-hotbar-actions.mdx`, `guides/plugin/listening-to-packets.mdx` | [customizing-hotbar-actions](https://hytalemodding.dev/en/docs/guides/plugin/customizing-hotbar-actions), [listening-to-packets](https://hytalemodding.dev/en/docs/guides/plugin/listening-to-packets) | +| `hytale-instances` | `guides/plugin/instances.mdx` | [instances](https://hytalemodding.dev/en/docs/guides/plugin/instances) | +| `hytale-inventory` | `guides/plugin/inventory-management.mdx` | [inventory-management](https://hytalemodding.dev/en/docs/guides/plugin/inventory-management) | +| `hytale-notifications` | `guides/plugin/send-notifications.mdx`, `server/entities.mdx` | [send-notifications](https://hytalemodding.dev/en/docs/guides/plugin/send-notifications), [entities](https://hytalemodding.dev/en/docs/server/entities) | +| `hytale-npc-templates` | `official-documentation/npc/` (all 12 chapters) | [npc](https://hytalemodding.dev/en/docs/official-documentation/npc) | +| `hytale-persistent-data` | `guides/plugin/store-persistent-data.mdx`, `guides/ecs/hytale-ecs-theory.mdx`, `guides/ecs/entity-component-system.mdx`, `guides/ecs/systems.mdx` | [store-persistent-data](https://hytalemodding.dev/en/docs/guides/plugin/store-persistent-data) | +| `hytale-player-death-event` | `guides/plugin/player-death-event.mdx` | [player-death-event](https://hytalemodding.dev/en/docs/guides/plugin/player-death-event) | +| `hytale-player-stats` | `guides/plugin/player-stats.mdx` | [player-stats](https://hytalemodding.dev/en/docs/guides/plugin/player-stats) | +| `hytale-playing-sounds` | `guides/plugin/playing-sounds.mdx`, `server/sounds.mdx` | [playing-sounds](https://hytalemodding.dev/en/docs/guides/plugin/playing-sounds), [sounds](https://hytalemodding.dev/en/docs/server/sounds) | +| `hytale-spawning-entities` | `guides/plugin/spawning-entities.mdx`, `server/entities.mdx` | [spawning-entities](https://hytalemodding.dev/en/docs/guides/plugin/spawning-entities), [entities](https://hytalemodding.dev/en/docs/server/entities) | +| `hytale-spawning-npcs` | `guides/plugin/spawning-npcs.mdx` | [spawning-npcs](https://hytalemodding.dev/en/docs/guides/plugin/spawning-npcs) | +| `hytale-teleporting-players` | `guides/plugin/teleporting-players.mdx` | [teleporting-players](https://hytalemodding.dev/en/docs/guides/plugin/teleporting-players) | +| `hytale-text-holograms` | `guides/plugin/text-hologram.mdx` | [text-hologram](https://hytalemodding.dev/en/docs/guides/plugin/text-hologram) | +| `hytale-ui-modding` | `official-documentation/custom-ui/common-styling.mdx`, `official-documentation/custom-ui/layout.mdx`, `official-documentation/custom-ui/markup.mdx`, `official-documentation/custom-ui/type-documentation/`, `guides/plugin/ui.mdx` | [custom-ui](https://hytalemodding.dev/en/docs/official-documentation/custom-ui) | +| `hytale-world-gen` | `guides/plugin/world-gen.mdx`, `official-documentation/worldgen/` (all subdirs) | [world-gen](https://hytalemodding.dev/en/docs/guides/plugin/world-gen) | +| `hytale-blocks` | `guides/plugin/creating-block.mdx` | [creating-block](https://hytalemodding.dev/en/docs/guides/plugin/creating-block) | +| `hytale-prefabs` | `guides/prefabs.mdx` | [prefabs](https://hytalemodding.dev/en/docs/guides/prefabs) | +| `hytale-commands` | `guides/plugin/creating-commands.mdx` | [creating-commands](https://hytalemodding.dev/en/docs/guides/plugin/creating-commands) | +| `hytale-items` | `guides/plugin/item-interaction.mdx`, `guides/plugin/item-registry.mdx` | [item-interaction](https://hytalemodding.dev/en/docs/guides/plugin/item-interaction), [item-registry](https://hytalemodding.dev/en/docs/guides/plugin/item-registry) | + +### Skills Without Upstream Doc URLs (Server Source Only) + +These skills were built primarily from decompiled server source and do not have matching pages on the docs site. Update them by reviewing changes in `lib/hytale-server/src/main/java/com/hypixel/`. + +| Skill | Primary Server Source Packages | +|-------|-------------------------------| +| `hytale-entity-effects` | `com.hypixel.server.ecs.components.effects`, `com.hypixel.server.entity.effect` | +| `hytale-logging` | `com.hypixel.server.log`, `com.hypixel.common.log` | +| `hytale-permissions` | `com.hypixel.server.permission` | +| `hytale-player-input` | `com.hypixel.server.network.packet`, `com.hypixel.server.input` | +| `hytale-plugin-config` | `com.hypixel.server.plugin` | +| `hytale-tag-system` | `com.hypixel.server.asset`, `com.hypixel.server.registry` | + +--- + +## Update Procedure + +### Step 1: Check for Upstream Changes + +Use the GitHub API to check recent commits affecting the docs content directory. Fetch the commit history for the content path: + +``` +https://api.github.com/repos/HytaleModding/site/commits?path=content/docs/en&since=YYYY-MM-DDTHH:MM:SSZ +``` + +Or check specific files using the raw content URL pattern: + +``` +https://raw.githubusercontent.com/HytaleModding/site/main/content/docs/en/{path} +``` + +**Practical approach using fetch_webpage:** + +1. Fetch the GitHub commits page for the content directory to see recent changes: + ``` + https://github.com/HytaleModding/site/commits/main/content/docs/en + ``` + +2. For each skill in the mapping table, fetch the raw MDX source for its mapped files: + ``` + https://raw.githubusercontent.com/HytaleModding/site/main/content/docs/en/guides/plugin/{filename}.mdx + ``` + +3. Compare the fetched content against the current skill's SKILL.md to identify: + - New API methods or classes documented + - Changed method signatures or parameters + - New code examples or updated examples + - Deprecated or removed functionality + - New sections or reorganized content + +### Step 2: Discover New Documentation Pages & Skills + +This is the most important step for keeping skills comprehensive. The HytaleModding site is community-driven and frequently adds new guides and documentation. + +#### 2a. Scan All Content Directories + +Fetch the directory listings for every content folder on GitHub to build a complete inventory of `.mdx` files: + +**Directories to scan:** +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/guides/plugin` +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/guides/ecs` +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/guides` (top-level guides like prefabs.mdx) +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation` +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/custom-ui` +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/npc` +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/official-documentation/worldgen` +- `https://github.com/HytaleModding/site/tree/main/content/docs/en/server` + +Also scan for **new subdirectories** that may have been added since the last check — these often indicate major new documentation categories. + +#### 2b. Classify Each File + +For every `.mdx` file found, classify it into one of these categories: + +| Category | Action | Examples | +|----------|--------|----------| +| **Already mapped** | Check for updates (Step 3) | Files in the Skill-to-Source Mapping table | +| **Meta/setup guide** | Skip — not skill material | `setting-up-env.mdx`, `build-and-test.mdx`, `browsing-serverjar.mdx` | +| **Extends existing skill** | Merge content into the existing skill | A new `item-registry.mdx` extending `hytale-items` | +| **New standalone topic** | **Create a new skill** (Step 2c) | A brand new guide on a topic with no skill | +| **New official docs section** | **Create a new skill** (Step 2c) | A new folder under `official-documentation/` | + +**How to decide "extends existing" vs "new standalone":** +- If the doc covers a sub-feature of something an existing skill already handles → extend existing +- If the doc introduces a fundamentally new system, API, or workflow → create new standalone skill +- When in doubt, check the existing skills' `description` field — if the new doc's trigger keywords overlap heavily, it probably extends an existing skill + +#### 2c. New Skill Discovery Procedure + +When you find an `.mdx` file (or new subdirectory) that doesn't map to any existing skill: + +**1. Fetch the raw MDX content:** +``` +https://raw.githubusercontent.com/HytaleModding/site/main/content/docs/en/{path-to-file}.mdx +``` + +**2. Analyze the content for skill viability:** + +A doc page is a good skill candidate if it has **at least 2** of these: +- Java code examples with specific API classes/methods +- JSON configuration examples +- A distinct system/feature not covered by existing skills +- Step-by-step procedures developers would need to reference +- API classes or methods that a developer would search for by name + +A doc page is NOT a good skill candidate if: +- It's purely conceptual with no actionable code/config +- It's a meta-guide about tooling setup (IDE, build, etc.) +- It duplicates content already in an existing skill +- It's a changelog or release notes page + +**3. Extract the skill scaffold from the MDX:** + +From the fetched MDX content, extract: +- **Title and topic** → becomes the skill `name` (prefixed with `hytale-`) +- **Key API classes and methods** → becomes `description` trigger keywords +- **Code examples** → becomes the skill's code reference sections +- **JSON examples** → becomes data-driven configuration sections +- **Concepts and terminology** → becomes Quick Reference table entries +- **Related upstream URLs** → becomes source references in the skill + +**4. Cross-reference with decompiled server source:** + +Search `lib/hytale-server/src/main/java/com/hypixel/` for the classes mentioned in the doc. This often reveals: +- Additional methods not documented yet +- Constructor signatures for proper usage +- Related classes the doc doesn't mention +- Package paths needed for import statements + +**5. Create the new skill following the template below (Step 5).** + +#### 2d. Skills Currently Identified as Without Matching Docs (Known Gaps) + +**Doc pages without skills (potential new skills):** + +| Doc File | Status | Recommendation | +|----------|--------|----------------| +| `guides/plugin/creating-block.mdx` | **DONE** — `hytale-blocks` | Block creation, asset packs, block JSON, textures, materials | +| `guides/prefabs.mdx` | **DONE** — `hytale-prefabs` | Prefab system, commands, reusable structures | +| `guides/plugin/item-interaction.mdx` | **DONE** — merged into `hytale-items` | Interaction content in `hytale-items` skill | +| `guides/plugin/item-registry.mdx` | **DONE** — merged into `hytale-items` | Registry content in `hytale-items` skill | +| `guides/plugin/client-inputs-reference.mdx` | Extends `hytale-player-input` | Merge reference content into `hytale-player-input` skill | +| `guides/plugin/listening-to-packets.mdx` | Extends `hytale-player-input` | Merge packet content into `hytale-player-input` skill | +| `guides/plugin/browsing-serverjar.mdx` | Meta-guide | Skip — not skill material | +| `guides/plugin/build-and-test.mdx` | Meta-guide | Skip — not skill material | +| `guides/plugin/setting-up-env.mdx` | Meta-guide | Skip — not skill material | + +**Update this table** each time you run the discovery process. Remove entries that have been addressed and add new ones found. + +### Step 3: Update Existing Skills + +For each skill with detected changes: + +1. **Fetch the raw MDX source** for all mapped files using: + ``` + https://raw.githubusercontent.com/HytaleModding/site/main/content/docs/en/{path} + ``` + +2. **Read the current SKILL.md** for the skill being updated. + +3. **Identify deltas** between the upstream content and the skill: + - New API methods, classes, or components → Add to skill + - Updated code examples → Replace stale examples + - Changed method signatures → Update references + - New sections or concepts → Incorporate into skill + - Removed/deprecated content → Remove or mark as deprecated + +4. **Cross-reference with decompiled server source** in `lib/hytale-server/src/main/java/com/hypixel/`: + - Verify that code examples in the updated docs are correct against the actual server JAR + - Check for API changes that the docs may not yet reflect + - Look for new classes or methods that supplement the doc content + +5. **Update the SKILL.md** following these rules: + - Preserve the skill's existing structure and organization style + - Keep the frontmatter `description` and triggers updated with any new keywords + - Ensure code examples compile against the current server version + - Update Quick Reference tables with new methods/approaches + - Maintain cross-references to related skills (`> **Related skills:** ...`) + - Do NOT remove project-specific guidance that was added beyond the upstream docs + +### Step 4: Update Server-Source-Only Skills + +For skills without upstream doc URLs: + +1. **Check `lib/` for changes** in the relevant packages (see mapping table above). + +2. **Look for new classes, methods, or changed signatures** in the decompiled source. + +3. **Update the skill** with: + - New API methods or classes + - Changed constructor or method parameters + - New component types or system types + - Updated code patterns + +### Step 5: Create New Skills + +When a new doc page is detected that warrants its own skill (see Step 2c), follow this complete procedure. + +#### 5a. Gather All Source Material + +1. **Fetch the raw MDX content** for the new page(s): + ``` + https://raw.githubusercontent.com/HytaleModding/site/main/content/docs/en/{path} + ``` + +2. **Search the decompiled server source** for key classes mentioned in the doc: + ``` + lib/hytale-server/src/main/java/com/hypixel/ + ``` + Use grep/search to find: + - The main API classes referenced in the doc + - Related classes in the same package + - Method signatures, constructors, and public fields + - Enum values or constants relevant to the topic + +3. **Check `lib/Server/` JSON files** for any data-driven definitions related to the topic: + - Entity definitions, block definitions, item definitions, etc. + - Configuration schemas and default values + +4. **Review existing skills** that are related to identify: + - Content boundaries (what this skill covers vs what existing skills cover) + - Cross-reference opportunities + - Shared patterns to maintain consistency + +#### 5b. Choose a Name + +Follow the naming convention: `hytale-{topic}` where `{topic}` is: +- Lowercase, hyphen-separated +- Describes the primary system or feature +- Matches what a developer would search for + +Examples: `hytale-blocks`, `hytale-prefabs`, `hytale-crafting`, `hytale-particles` + +#### 5c. Create the Skill Using the Template + +Create the directory and `SKILL.md`: +``` +.github/skills/hytale-{topic}/ +└── SKILL.md +``` + +**Use this template for the SKILL.md content:** + +`````markdown +--- +name: hytale-{topic} +description: {One sentence describing what this skill covers}. Use when {common use cases}. Triggers - {keyword1}, {keyword2}, {ClassName1}, {ClassName2}, {method1}, {concept1}. +--- + +# Hytale {Topic Title} + +{1-2 sentence summary of when and why to use this skill.} + +> **Source:** <{upstream doc URL}> +> **Related skills:** For {related concept}, see `{related-skill-name}`. + +--- + +## Quick Reference + +| Task | Approach | +|------|----------| +| {Common task 1} | `{method or approach}` | +| {Common task 2} | `{method or approach}` | +| {Common task 3} | `{method or approach}` | + +--- + +## Key Concepts + +### {Concept 1} + +{Explanation of the concept and how it fits into the system.} + +### {Concept 2} + +{Explanation with relevant details.} + +--- + +## Required Imports + +```java +import com.hypixel.{...}; +``` + +--- + +## Code Examples + +### {Example 1 Title} + +```java +// Full working example with imports context +``` + +### {Example 2 Title} + +```java +// Full working example +``` + +--- + +## JSON Configuration (if applicable) + +```json +{ + "example": "configuration" +} +``` + +--- + +## Edge Cases & Gotchas + +- {Important caveat 1} +- {Important caveat 2} +- {Thread safety note if applicable — use `world.execute()` pattern} +``` +````` + +#### 5d. Write the Description (Critical) + +The `description` field in the frontmatter is what triggers skill selection. It must contain: + +1. **What it does** — First sentence summarizes the skill's purpose +2. **When to use it** — "Use when..." clause with common scenarios +3. **Trigger keywords** — "Triggers - " followed by ALL relevant keywords, including: + - Plain English terms developers would search for (e.g., "block", "create block") + - Java class names (e.g., `BlockComponent`, `ChunkStore`) + - Method names (e.g., `registerBlock`, `setBlock`) + - JSON-related terms if applicable (e.g., "block JSON", "block definition") + +**Good example:** +``` +description: Documents how to create custom blocks in Hytale plugins using BlockComponent and ChunkStore. Use when creating blocks, defining block properties, registering block components, or working with block JSON definitions. Triggers - block, create block, custom block, BlockComponent, ChunkStore, block JSON, block definition, registerBlock, block properties, block tick, setTicking. +``` + +**Bad example (too vague, missing triggers):** +``` +description: Information about blocks in Hytale. +``` + +#### 5e. Fill in Content from Source Material + +Working through the template sections: + +1. **Quick Reference** — Extract the most common tasks from the doc and provide one-liner solutions +2. **Key Concepts** — Summarize the core ideas, focusing on what's unique to this system +3. **Required Imports** — List ALL imports needed for the code examples (full package paths from decompiled source) +4. **Code Examples** — Adapt examples from the MDX, verify against decompiled source, add context comments +5. **JSON Configuration** — Include any JSON definitions from the doc or from `lib/Server/` examples +6. **Edge Cases** — Extract warnings, caveats, and thread-safety notes from the doc and your server source review + +#### 5f. Register the Skill + +After creating the SKILL.md: + +1. **Add to `.github/copilot-instructions.md`** in copilot-instructions: + - Add a `` entry in the `` section following the existing pattern + - Include the `name`, `description` (matching frontmatter), and `file` path + +2. **Update this skill's mapping table** — Add the new skill to the "Skills with Upstream Doc URLs" table in this file (update-hytale-skills SKILL.md) + +3. **Update the known gaps table** (Step 2d) — Move the entry from "NEW SKILL CANDIDATE" to the mapping table + +#### 5g. Validation + +After creating and registering: + +- [ ] Skill directory name matches the `name` field in frontmatter +- [ ] Description contains meaningful trigger keywords (at least 8-10 keywords) +- [ ] Code examples use correct import paths verified against `lib/` +- [ ] Quick Reference table has at least 3 entries +- [ ] At least one complete, working code example +- [ ] Related skills cross-references are bidirectional (update the related skills too) +- [ ] Skill registered in `.github/copilot-instructions.md` +- [ ] Mapping table in this skill updated + +--- + +## Post-Update Checklist + +After updating any skills, verify: + +- [ ] **Description updated** — New trigger keywords added if the upstream added new concepts +- [ ] **Code examples compile** — Run the build task to verify no compilation errors from example code +- [ ] **Cross-references valid** — Related skill references still point to existing skills +- [ ] **URLs updated** — Any referenced doc URLs still resolve correctly +- [ ] **No duplicate content** — Changes didn't introduce overlap with other skills +- [ ] **Server source alignment** — Code examples match the decompiled server API in `lib/` +- [ ] **copilot-instructions.md updated** — New skills added to the skill list with correct description and trigger info + +--- + +## Automation Tips + +### Batch Checking All Skills + +To check all skills at once, iterate through the mapping table and fetch each source file. Compare modification dates or content hashes to quickly identify which skills need attention. + +### Prioritizing Updates + +1. **High priority:** Skills where the upstream API or method signatures changed (breaking changes) +2. **Medium priority:** Skills where new features or methods were added +3. **Low priority:** Skills where only prose, typos, or formatting changed in the docs + +### Change Detection Heuristic + +When comparing upstream MDX to the current SKILL.md, focus on: +- Java code blocks (` ```java ... ``` `) — These contain API examples most likely to change +- Class and method names mentioned in prose +- JSON configuration examples +- New headings or sections that indicate new features + +--- + +## Full Discovery & Update Workflow (Quick-Start) + +Use this as a single checklist when running the full update cycle: + +### Phase 1: Scan +- [ ] Fetch `https://github.com/HytaleModding/site/commits/main/content/docs/en` — note which files changed recently +- [ ] Fetch directory listings for all content directories (Step 2a) +- [ ] List all `.mdx` files found across all directories +- [ ] Compare against the Skill-to-Source Mapping table +- [ ] Classify each new/unmapped file (Step 2b) + +### Phase 2: Discover New Skills +- [ ] For each "NEW SKILL CANDIDATE" file, fetch raw MDX content +- [ ] Evaluate viability using the criteria in Step 2c +- [ ] For viable candidates, search decompiled source for related classes +- [ ] Create new skills using the template (Step 5c) +- [ ] Write descriptions with proper trigger keywords (Step 5d) +- [ ] Register new skills and update mapping tables (Step 5f) + +### Phase 3: Update Existing Skills +- [ ] For each skill with upstream changes, fetch the raw MDX source +- [ ] Compare against current SKILL.md content section by section +- [ ] Update code examples, method signatures, and Quick Reference tables +- [ ] Cross-reference against decompiled server source in `lib/` +- [ ] Update description trigger keywords if new concepts were added + +### Phase 4: Server-Source-Only Skills +- [ ] Check `lib/hytale-server/` for changes in relevant packages +- [ ] Update skills with new/changed APIs from decompiled source + +### Phase 5: Validate +- [ ] Run the build task to ensure no compilation errors +- [ ] Verify all cross-references between skills are bidirectional +- [ ] Confirm all doc URLs still resolve +- [ ] Ensure `copilot-instructions.md` reflects all current skills diff --git a/skills/update-server-lib/SKILL.md b/skills/update-server-lib/SKILL.md new file mode 100644 index 0000000..a072b96 --- /dev/null +++ b/skills/update-server-lib/SKILL.md @@ -0,0 +1,125 @@ +--- +name: update-server-lib +description: Updates the Hytale server reference files in lib/ by downloading the latest pre-release server, decompiling the JAR using Vineflower, and updating server assets. Use when needing to update to a new Hytale server version, refreshing decompiled source code, or syncing with the latest pre-release. Triggers - update server, download server, decompile jar, vineflower, update lib, new server version, sync server, refresh server. +--- + +# Update Server Lib Skill + +Updates the `lib/` folder with the latest Hytale pre-release server files including decompiled source code and server assets. + +## Prerequisites + +Before running these scripts, ensure the following are installed and on PATH: + +- **Hytale Downloader**: The `hytale-downloader-windows-amd64.exe` binary (already authenticated). Default location: `C:\hytale-downloader\` (configurable via `HYTALE_DOWNLOADER_PATH` env var) +- **Python 3+**: For running the patcher tool (`py --version` or `python --version`) +- **Java 25+**: `java --version` should show 25.x +- **Maven**: `mvn --version` should work +- **Git**: `git --version` should work + +## Directory Structure + +``` +\ # Default: C:\hytale-downloader\ +├── hytale-downloader-windows-amd64.exe +├── .hytale-downloader-credentials.json +├── downloads\ # Created by script +│ └── .zip # Downloaded server package +└── extracted\ # Created by script + └── \ # Extracted server files + ├── Server\ + │ └── HytaleServer.jar + └── Assets\ + └── Server\ + +%APPDATA%\Hytale\install\pre-release\package\game\ +└── latest\ # Symlink to current build + └── Client\Data\Game\Interface\ # UI source (.ui files) +``` + +## Usage + +Run the CMD scripts from anywhere (they use absolute paths): + +### Full Update (Recommended) + +```cmd +.\.github\skills\update-server-lib\scripts\Full-Update.cmd +``` + +This runs both steps in sequence. + +### Step 1: Download and Extract Latest Pre-Release + +```cmd +.\.github\skills\update-server-lib\scripts\Download-Server.cmd +``` + +This script: +- Downloads the latest pre-release server using the Hytale downloader +- Extracts the server zip file +- Extracts the Assets.zip within it +- Saves the version for the next step + +### Step 2: Decompile and Update Lib + +```cmd +.\.github\skills\update-server-lib\scripts\Update-Lib.cmd +``` + +Or specify a version: + +```cmd +.\.github\skills\update-server-lib\scripts\Update-Lib.cmd 2026.01.29-301e13929 +``` + +This script: +- Clones/updates the HytaleModding/patcher tool +- Sets up Python virtual environment +- Runs Vineflower decompilation on HytaleServer.jar +- Copies decompiled source to `lib/hytale-server/src/main/java` +- Copies Server assets to `lib/Server` +- Copies UI assets to `lib/UI` +- Updates HytaleServer.jar in lib root + +## Script Configuration + +Set the `HYTALE_DOWNLOADER_PATH` environment variable to override the default downloader location. All sub-paths are derived from it automatically. + +```cmd +REM Example: set before running scripts, or add to your system environment variables +set HYTALE_DOWNLOADER_PATH=D:\my-hytale-tools +``` + +| Setting | Default | Description | +|---------|---------|-------------| +| `HYTALE_DOWNLOADER_PATH` | `C:\hytale-downloader` | Path to hytale-downloader folder (env var) | +| `DOWNLOAD_DIR` | `\downloads` | Where to save downloaded zips | +| `EXTRACT_DIR` | `\extracted` | Where to extract server files | +| `PATCHER_DIR` | `\patcher` | Where to clone/use patcher tool | +| `PATCHLINE` | `pre-release` | Patchline to download from | + +## Troubleshooting + +### Authentication Errors +If you get 401 or authentication errors, delete `.hytale-downloader-credentials.json` in your downloader directory (default: `C:\hytale-downloader\`) and run the downloader manually to re-authenticate. + +### Decompilation Fails +- Ensure Python 3.13+ is installed: `py -3.13 --version` +- Ensure Java 25 is on PATH: `java --version` +- Ensure Maven is on PATH: `mvn --version` +- Check the patcher output for specific errors + +### Incomplete Extraction +If extraction fails, delete the partially extracted folder and run again. + +## Version Tracking + +After a successful update, the script creates `.github/skills/update-server-lib/LAST_VERSION.txt` with the downloaded version for reference. + +## Notes + +- The decompiled code may have compilation errors - this is expected. It's for reference/exploration only. +- Server assets in `lib/Server` are read-only references; don't modify them directly. +- UI assets in `lib/UI` are for reference when building custom UIs. +- Always test your plugin after updating to ensure compatibility with the new server version. diff --git a/skills/update-server-lib/scripts/Download-Server.cmd b/skills/update-server-lib/scripts/Download-Server.cmd new file mode 100644 index 0000000..a61bc38 --- /dev/null +++ b/skills/update-server-lib/scripts/Download-Server.cmd @@ -0,0 +1,137 @@ +@echo off +setlocal enabledelayedexpansion + +REM ============================================ +REM Hytale Server Downloader +REM Downloads and extracts the latest pre-release server +REM ============================================ + +REM Use HYTALE_DOWNLOADER_PATH env var if set, otherwise default +if not defined HYTALE_DOWNLOADER_PATH set "HYTALE_DOWNLOADER_PATH=C:\hytale-downloader" +set "DOWNLOADER_PATH=%HYTALE_DOWNLOADER_PATH%" +set "DOWNLOAD_DIR=%DOWNLOADER_PATH%\downloads" +set "EXTRACT_DIR=%DOWNLOADER_PATH%\extracted" +set "PATCHLINE=pre-release" + +REM Create directories +if not exist "%DOWNLOAD_DIR%" mkdir "%DOWNLOAD_DIR%" +if not exist "%EXTRACT_DIR%" mkdir "%EXTRACT_DIR%" + +set "DOWNLOADER_EXE=%DOWNLOADER_PATH%\hytale-downloader-windows-amd64.exe" + +if not exist "%DOWNLOADER_EXE%" ( + echo ERROR: Hytale downloader not found at: %DOWNLOADER_EXE% + exit /b 1 +) + +echo ============================================ +echo Hytale Server Downloader +echo ============================================ +echo. +echo Patchline: %PATCHLINE% +echo. + +REM Generate timestamp for unique filename +for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd-HHmmss"') do set "TIMESTAMP=%%i" +if "%TIMESTAMP%"=="" set "TIMESTAMP=download" +set "DOWNLOAD_ZIP=%DOWNLOAD_DIR%\server-%PATCHLINE%-%TIMESTAMP%.zip" + +echo Downloading server package... +echo Download path: %DOWNLOAD_ZIP% +echo. + +REM Change to downloader directory for credentials file +REM Capture output to parse version +set "DL_OUTPUT=%TEMP%\hytale-dl-output-%TIMESTAMP%.txt" +pushd "%DOWNLOADER_PATH%" +"%DOWNLOADER_EXE%" -patchline %PATCHLINE% -download-path "%DOWNLOAD_ZIP%" -skip-update-check > "%DL_OUTPUT%" 2>&1 +set "DL_RESULT=!ERRORLEVEL!" +type "%DL_OUTPUT%" +popd + +if not exist "%DOWNLOAD_ZIP%" ( + echo ERROR: Download failed - zip file not found at: %DOWNLOAD_ZIP% + if exist "%DL_OUTPUT%" del "%DL_OUTPUT%" + exit /b 1 +) + +echo. +echo Download complete! +echo. + +REM Parse version from downloader output (e.g., "version 2026.01.29-301e13929") +set "SERVER_VERSION=" +for /f "tokens=2 delims=()" %%v in ('findstr /i "version" "%DL_OUTPUT%"') do ( + set "VER_LINE=%%v" + for /f "tokens=2" %%w in ("!VER_LINE!") do ( + set "SERVER_VERSION=%%w" + ) +) +if exist "%DL_OUTPUT%" del "%DL_OUTPUT%" + +if "%SERVER_VERSION%"=="" ( + set "SERVER_VERSION=%TIMESTAMP%" + echo Could not parse version, using timestamp: %SERVER_VERSION% +) else ( + echo Server version: %SERVER_VERSION% +) + +REM Rename zip to version +set "VERSIONED_ZIP=%DOWNLOAD_DIR%\%SERVER_VERSION%.zip" +if not "%DOWNLOAD_ZIP%"=="%VERSIONED_ZIP%" ( + if exist "%VERSIONED_ZIP%" del /f "%VERSIONED_ZIP%" + move "%DOWNLOAD_ZIP%" "%VERSIONED_ZIP%" >nul 2>&1 + if exist "%VERSIONED_ZIP%" set "DOWNLOAD_ZIP=%VERSIONED_ZIP%" +) + +REM Extract the main server zip +set "SERVER_EXTRACT_PATH=%EXTRACT_DIR%\%SERVER_VERSION%" +if exist "%SERVER_EXTRACT_PATH%" ( + echo Removing existing extracted folder... + rmdir /s /q "%SERVER_EXTRACT_PATH%" +) + +echo. +echo Extracting server package to: %SERVER_EXTRACT_PATH% +powershell -NoProfile -Command "Expand-Archive -Path '%DOWNLOAD_ZIP%' -DestinationPath '%SERVER_EXTRACT_PATH%' -Force" +if !ERRORLEVEL! neq 0 ( + echo ERROR: Failed to extract server package + exit /b 1 +) +echo Main package extracted! +echo. + +REM Find and extract Assets.zip +set "ASSETS_ZIP=" +for /r "%SERVER_EXTRACT_PATH%" %%f in (Assets.zip) do ( + if exist "%%f" set "ASSETS_ZIP=%%f" +) + +if defined ASSETS_ZIP ( + echo Extracting Assets.zip... + set "ASSETS_DIR=%SERVER_EXTRACT_PATH%\Assets" + powershell -NoProfile -Command "Expand-Archive -Path '!ASSETS_ZIP!' -DestinationPath '!ASSETS_DIR!' -Force" + if !ERRORLEVEL! neq 0 ( + echo WARNING: Failed to extract Assets.zip + ) else ( + echo Assets extracted! + ) +) else ( + echo Warning: Assets.zip not found in extracted files +) + +echo. +echo ============================================ +echo Download Complete +echo ============================================ +echo. +echo Version: %SERVER_VERSION% +echo Extracted to: %SERVER_EXTRACT_PATH% +echo. +echo Next step: Run Update-Lib.cmd to decompile and update lib folder +echo. + +REM Save version for Update-Lib.cmd +echo %SERVER_VERSION%> "%DOWNLOAD_DIR%\LATEST_VERSION.txt" + +exit /b 0 diff --git a/skills/update-server-lib/scripts/Full-Update.cmd b/skills/update-server-lib/scripts/Full-Update.cmd new file mode 100644 index 0000000..91cff69 --- /dev/null +++ b/skills/update-server-lib/scripts/Full-Update.cmd @@ -0,0 +1,51 @@ +@echo off +setlocal + +REM ============================================ +REM Hytale Server Full Update +REM Downloads, decompiles, and updates lib +REM ============================================ + +set "SCRIPT_DIR=%~dp0" + +echo ============================================ +echo Hytale Server Full Update +echo ============================================ +echo. + +echo ^>^>^> Step 1/2: Downloading server... +echo. + +call "%SCRIPT_DIR%Download-Server.cmd" +if errorlevel 1 ( + echo. + echo ERROR: Download step failed + exit /b 1 +) + +echo. +echo ^>^>^> Step 2/2: Updating lib folder... +echo. + +call "%SCRIPT_DIR%Update-Lib.cmd" +if errorlevel 1 ( + echo. + echo ERROR: Update step failed + exit /b 1 +) + +echo. +echo ============================================ +echo Full Update Complete! +echo ============================================ +echo. +echo Your lib folder is now updated with: +echo - Latest HytaleServer.jar +echo - Decompiled source code (for reference) +echo - Server assets +echo - UI assets +echo. +echo Run 'Build and Deploy Plugin' task to test your plugin! +echo. + +endlocal diff --git a/skills/update-server-lib/scripts/Update-Lib.cmd b/skills/update-server-lib/scripts/Update-Lib.cmd new file mode 100644 index 0000000..16f0e28 --- /dev/null +++ b/skills/update-server-lib/scripts/Update-Lib.cmd @@ -0,0 +1,327 @@ +@echo off +setlocal enabledelayedexpansion + +REM ============================================ +REM Hytale Server Lib Updater +REM Decompiles and updates lib folder +REM ============================================ + +REM Use HYTALE_DOWNLOADER_PATH env var if set, otherwise default +if not defined HYTALE_DOWNLOADER_PATH set "HYTALE_DOWNLOADER_PATH=C:\hytale-downloader" +set "EXTRACT_DIR=%HYTALE_DOWNLOADER_PATH%\extracted" +set "PATCHER_DIR=%HYTALE_DOWNLOADER_PATH%\patcher" +set "DOWNLOAD_DIR=%HYTALE_DOWNLOADER_PATH%\downloads" + +REM Get workspace root (4 levels up from script location) +set "SCRIPT_DIR=%~dp0" +for %%I in ("%SCRIPT_DIR%\..\..\..\..\") do set "WORKSPACE_ROOT=%%~fI" +set "LIB_DIR=%WORKSPACE_ROOT%lib" + +echo ============================================ +echo Hytale Server Lib Updater +echo ============================================ +echo. +echo Workspace: %WORKSPACE_ROOT% +echo Lib Dir: %LIB_DIR% +echo. + +REM Get server version - either from argument or latest +set "SERVER_VERSION=%~1" +if "%SERVER_VERSION%"=="" ( + if exist "%DOWNLOAD_DIR%\LATEST_VERSION.txt" ( + set /p SERVER_VERSION=<"%DOWNLOAD_DIR%\LATEST_VERSION.txt" + ) +) + +if "%SERVER_VERSION%"=="" ( + REM Find latest folder in extract dir + for /f "tokens=*" %%d in ('dir /b /ad /o-n "%EXTRACT_DIR%" 2^>nul') do ( + set "SERVER_VERSION=%%d" + goto :found_version + ) +) +:found_version + +if "%SERVER_VERSION%"=="" ( + echo ERROR: No server version found. Run Download-Server.cmd first. + exit /b 1 +) + +set "SERVER_EXTRACT_PATH=%EXTRACT_DIR%\%SERVER_VERSION%" +if not exist "%SERVER_EXTRACT_PATH%" ( + echo ERROR: Server version folder not found: %SERVER_EXTRACT_PATH% + exit /b 1 +) + +echo Using version: %SERVER_VERSION% +echo. + +REM Find HytaleServer.jar +set "HYTALE_JAR=" +for /r "%SERVER_EXTRACT_PATH%" %%f in (HytaleServer.jar) do ( + if exist "%%f" set "HYTALE_JAR=%%f" +) + +if not defined HYTALE_JAR ( + echo ERROR: HytaleServer.jar not found in: %SERVER_EXTRACT_PATH% + exit /b 1 +) + +echo Found HytaleServer.jar: %HYTALE_JAR% + +REM Find Assets folder +set "ASSETS_PATH=" +for /d /r "%SERVER_EXTRACT_PATH%" %%d in (*) do ( + if /i "%%~nxd"=="Assets" ( + if exist "%%d\Server" set "ASSETS_PATH=%%d" + ) +) + +if defined ASSETS_PATH ( + echo Found Assets: %ASSETS_PATH% +) + +echo. +echo ============================================ +echo Checking Prerequisites +echo ============================================ +echo. + +set "PREREQ_FAIL=" +set "PYTHON_CMD=py -3" + +REM Check Python +where py >nul 2>nul +if errorlevel 1 ( + where python >nul 2>nul + if errorlevel 1 ( + echo [FAIL] Python not found + set "PREREQ_FAIL=1" + ) else ( + echo [OK] Python found + set "PYTHON_CMD=python" + ) +) else ( + echo [OK] Python found +) + +REM Check Java +where java >nul 2>nul +if errorlevel 1 ( + echo [FAIL] Java not found + set "PREREQ_FAIL=1" +) else ( + echo [OK] Java found +) + +REM Check Maven +where mvn >nul 2>nul +if errorlevel 1 ( + echo [FAIL] Maven not found + set "PREREQ_FAIL=1" +) else ( + echo [OK] Maven found +) + +REM Check Git +where git >nul 2>nul +if errorlevel 1 ( + echo [FAIL] Git not found + set "PREREQ_FAIL=1" +) else ( + echo [OK] Git found +) + +REM Check jar command +where jar >nul 2>nul +if errorlevel 1 ( + echo [FAIL] jar command not found + set "PREREQ_FAIL=1" +) else ( + echo [OK] jar found +) + +if defined PREREQ_FAIL ( + echo. + echo ERROR: Prerequisites check failed. Please install missing tools. + exit /b 1 +) + +echo. +echo ============================================ +echo Setting up Patcher Tool +echo ============================================ +echo. + +REM Clone or update patcher repo +if exist "%PATCHER_DIR%" ( + echo Updating patcher repository... + pushd "%PATCHER_DIR%" + git pull --ff-only + popd +) else ( + echo Cloning patcher repository... + git clone "https://github.com/HytaleModding/patcher.git" "%PATCHER_DIR%" + if errorlevel 1 ( + echo ERROR: Failed to clone patcher repository + exit /b 1 + ) +) + +REM Setup Python venv +set "VENV_PATH=%PATCHER_DIR%\.venv" +set "VENV_PYTHON=%VENV_PATH%\Scripts\python.exe" + +if not exist "%VENV_PYTHON%" ( + echo. + echo Creating Python virtual environment... + pushd "%PATCHER_DIR%" + %PYTHON_CMD% -m venv .venv + popd +) + +REM Install requirements +echo. +echo Installing Python dependencies... +if exist "%PATCHER_DIR%\requirements.txt" ( + "%VENV_PYTHON%" -m pip install -r "%PATCHER_DIR%\requirements.txt" --quiet +) + +REM Copy HytaleServer.jar to patcher directory +echo. +echo Copying HytaleServer.jar to patcher... +copy /y "%HYTALE_JAR%" "%PATCHER_DIR%\HytaleServer.jar" >nul + +echo. +echo ============================================ +echo Running Decompilation +echo ============================================ +echo. + +REM Check if patcher already has decompiled output - if so, clean it for fresh decompile +set "PATCHER_OUTPUT=%PATCHER_DIR%\hytale-server" +if exist "%PATCHER_OUTPUT%" ( + echo Cleaning previous decompilation output... + rmdir /s /q "%PATCHER_OUTPUT%" +) + +REM Also clean work directory for fresh decompile +if exist "%PATCHER_DIR%\work" ( + rmdir /s /q "%PATCHER_DIR%\work" +) + +echo This may take several minutes... +echo Decompiling com.hypixel package using Vineflower... +echo. + +pushd "%PATCHER_DIR%" +set "HYTALESERVER_JAR_PATH=%PATCHER_DIR%\HytaleServer.jar" +"%VENV_PYTHON%" run.py setup +set "DECOMPILE_RESULT=!ERRORLEVEL!" +popd + +if !DECOMPILE_RESULT! neq 0 ( + echo. + echo ERROR: Decompilation failed with exit code: !DECOMPILE_RESULT! + exit /b 1 +) + +echo. +echo Decompilation complete! + +echo. +echo ============================================ +echo Updating lib folder +echo ============================================ +echo. + +REM Copy decompiled source +set "DECOMPILE_PATH=%PATCHER_DIR%\hytale-server\src\main\java\com" +set "LIB_SERVER_SRC=%LIB_DIR%\hytale-server\src\main\java" + +if exist "%DECOMPILE_PATH%" ( + echo Copying decompiled source code... + + if exist "%LIB_SERVER_SRC%\com" ( + echo Removing existing source... + rmdir /s /q "%LIB_SERVER_SRC%\com" + ) + + if not exist "%LIB_SERVER_SRC%" mkdir "%LIB_SERVER_SRC%" + + echo Copying new source... + xcopy /s /e /i /q "%DECOMPILE_PATH%" "%LIB_SERVER_SRC%\com" >nul + + echo Source code copied to: %LIB_SERVER_SRC% +) else ( + echo Warning: Decompiled source not found at: %DECOMPILE_PATH% +) + +REM Copy HytaleServer.jar +echo. +echo Copying HytaleServer.jar... +copy /y "%HYTALE_JAR%" "%LIB_DIR%\HytaleServer.jar" >nul +echo JAR copied to: %LIB_DIR%\HytaleServer.jar + +REM Copy Server assets +if defined ASSETS_PATH ( + if exist "%ASSETS_PATH%\Server" ( + echo. + echo Copying Server assets... + + if exist "%LIB_DIR%\Server" ( + echo Removing existing Server assets... + rmdir /s /q "%LIB_DIR%\Server" + ) + + xcopy /s /e /i /q "%ASSETS_PATH%\Server" "%LIB_DIR%\Server" >nul + echo Server assets copied to: %LIB_DIR%\Server + ) +) + +REM Copy UI assets from Hytale launcher installation (has the actual .ui files) +REM Uses the 'latest' symlink which points to current build +set "UI_SOURCE=%APPDATA%\Hytale\install\pre-release\package\game\latest\Client\Data\Game\Interface" + +if exist "%UI_SOURCE%" ( + echo. + echo Copying UI assets from Hytale installation... + echo Source: %UI_SOURCE% + + if exist "%LIB_DIR%\UI" ( + echo Removing existing UI assets... + rmdir /s /q "%LIB_DIR%\UI" + ) + + xcopy /s /e /i /q "%UI_SOURCE%" "%LIB_DIR%\UI" >nul + echo UI assets copied to: %LIB_DIR%\UI +) else ( + echo Warning: UI folder not found at: %UI_SOURCE% + echo Make sure Hytale is installed via the launcher. +) + +REM Save version info +echo %SERVER_VERSION%> "%SCRIPT_DIR%..\LAST_VERSION.txt" + +echo. +echo ============================================ +echo Update Complete +echo ============================================ +echo. +echo Updated to version: %SERVER_VERSION% +echo. +echo Lib folder structure: +echo lib/ +echo HytaleServer.jar (original JAR) +echo hytale-server/src/main/ (decompiled source) +echo Server/ (server assets) +echo UI/ (UI assets) +echo. +echo Remember: Decompiled code may have errors - it's for reference only. +echo. +echo Next steps: +echo 1. Review changes with: git diff lib/ +echo 2. Test your plugin with: Build and Deploy Plugin task +echo. + +exit /b 0 diff --git a/skills/validate-agent-files/SKILL.md b/skills/validate-agent-files/SKILL.md new file mode 100644 index 0000000..66f14f3 --- /dev/null +++ b/skills/validate-agent-files/SKILL.md @@ -0,0 +1,203 @@ +--- +name: validate-agent-files +description: Validates AI coding assistant customization files (agents, skills, prompts, instructions) for correct format and structure. Works with GitHub Copilot, Claude Code, Codex, OpenCode, and other providers. Use when checking if agent files are properly configured, troubleshooting agent issues, or before committing new customization files. +--- + +# Validate Agent Files + +Validates that agent, skill, prompt, and instruction files follow the correct format and structure. + +## Provider Folder Reference + +This skill works across multiple AI coding assistant providers: + +| Provider | Base Folder | +|----------|-------------| +| GitHub Copilot | `.github/` | +| Claude Code | `.claude/` | +| Codex | `.codex/` | +| OpenCode | `.config/opencode/` | + +**Throughout this document, `/` represents your chosen provider's base folder.** + +## When to Use + +- Before committing new agents, skills, prompts, or instructions +- When an agent isn't behaving as expected +- To audit existing customization files for issues +- After modifying any `.github` customization files + +## Validation Process + +### Step 1: Identify File Type + +Determine the type based on location and extension: +- `/agents/*.md` → Agent file (user-invokable) +- `/agents/*.subagent.agent.md` → Sub-agent file (workflow component) +- `/skills/*/SKILL.md` → Skill file +- `/prompts/*.prompt.md` → Prompt file +- `/instructions/*.instructions.md` → Instruction file + +### Step 2: Apply Type-Specific Validation + +## Agent File Validation (`/agents/*.md`) + +**Required Structure:** +```yaml +--- +name: agent-name +description: When to use this agent (should include examples) +user-invokable: true # Optional, defaults to true +--- + +[System prompt body] +``` + +**Supported Frontmatter Attributes:** +- `name` (required) - Agent identifier +- `description` (required) - When/how to use, with examples +- `user-invokable` (optional) - Set to `false` for sub-agents (default: `true`) +- `tools` - List of allowed tools +- `model` - Specific model to use +- `handoffs` - Other agents this can delegate to + +**Checks:** +1. ✓ YAML frontmatter present with `---` delimiters +2. ✓ `name` field exists and is non-empty +3. ✓ `description` field exists (recommend 50+ characters with examples) +4. ✓ Body content exists after frontmatter +5. ✓ If `tools` specified, they are valid tool names +6. ✓ If filename contains `.subagent.agent.md`, verify `user-invokable: false` is set + +**Naming Convention Checks:** +- User-facing agents: `.agent.md` or `.md` +- Sub-agents: `.subagent.agent.md` with `user-invokable: false` + +**Common Issues:** +- Missing `---` delimiters +- Empty or minimal description +- No usage examples in description +- Body content missing or too brief +- Sub-agent missing `user-invokable: false` +- Sub-agent not using `.subagent.agent.md` naming convention + +## Skill File Validation (`/skills/*/SKILL.md`) + +**Required Structure:** +```yaml +--- +name: skill-name +description: What this skill does and when to use it. +--- + +[Skill instructions body] +``` + +**Supported Frontmatter Attributes:** +- `name` (required) - Must match parent directory name, lowercase with hyphens +- `description` (required) - Max 1024 chars, describes function and triggers +- `license` (optional) - License information +- `compatibility` (optional) - Environment requirements +- `metadata` (optional) - Key-value pairs for additional info +- `allowed-tools` (optional) - Space-delimited pre-approved tools + +**Checks:** +1. ✓ File is named `SKILL.md` inside a directory +2. ✓ `name` matches parent directory name exactly +3. ✓ `name` is lowercase, alphanumeric with hyphens only +4. ✓ `name` doesn't start/end with hyphen or have consecutive hyphens +5. ✓ `description` is 1-1024 characters +6. ✓ Body content provides clear instructions + +**Common Issues:** +- `name` doesn't match directory name +- Uppercase characters in name +- Description too vague (should include trigger keywords) +- Missing instructions in body + +## Prompt File Validation (`/prompts/*.prompt.md`) + +**Required Structure:** +```yaml +--- +mode: agent +description: What this prompt does +--- + +[Prompt template with {{variables}}] +``` + +**Supported Frontmatter Attributes:** +- `mode` (optional) - One of: `agent` (default), `ask`, `edit`, `generate` +- `tools` (optional) - Available tools for this prompt +- `description` (optional but recommended) - What the prompt accomplishes + +**Checks:** +1. ✓ File has `.prompt.md` extension +2. ✓ If `mode` present, it's a valid value +3. ✓ Variables use `{{variableName}}` syntax +4. ✓ Body content exists (the prompt itself) + +**Common Issues:** +- Wrong extension (`.md` instead of `.prompt.md`) +- Invalid `mode` value +- Undefined variables in template + +## Instruction File Validation (`/instructions/*.instructions.md`) + +**Required Structure:** +```yaml +--- +applyTo: "**/*.ts" +--- + +[Contextual instructions] +``` + +**Supported Frontmatter Attributes:** +- `applyTo` (required) - Glob pattern(s) for when instructions apply + +**Checks:** +1. ✓ File has `.instructions.md` extension +2. ✓ `applyTo` field exists +3. ✓ `applyTo` contains valid glob pattern(s) +4. ✓ Body content provides meaningful guidance + +**Common Issues:** +- Wrong extension +- Missing `applyTo` field +- Invalid glob syntax +- Empty or minimal instructions + +## Output Format + +```markdown +## Validation: [filename] + +**Type:** [Agent|Skill|Prompt|Instruction] +**Status:** ✅ Valid | ⚠️ Warnings | ❌ Invalid + +### Issues +- [Issue 1 with line number if applicable] +- [Issue 2] + +### Recommendations +- [Suggestion for improvement] +``` + +## Batch Validation + +When validating all files, provide summary: + +```markdown +## Validation Summary + +| Type | Total | Valid | Warnings | Invalid | +|------|-------|-------|----------|---------| +| Agents | X | X | X | X || Sub-Agents | X | X | X | X || Skills | X | X | X | X | +| Prompts | X | X | X | X | +| Instructions | X | X | X | X | + +### Files Requiring Attention +- [List files with issues] +``` diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index 136122c..cb77b67 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -36,6 +36,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.UUID; @@ -44,14 +45,23 @@ */ public class TerminalInventoryPage extends InteractiveCustomUIPage { private static final String LAYOUT_PATH = "Pages/HytechTerminalPage.ui"; - private static final int PLAYER_STORAGE_VISIBLE_SLOTS = 30; - private static final int PLAYER_HOTBAR_VISIBLE_SLOTS = 10; + private static final int PLAYER_STORAGE_VISIBLE_SLOTS = 36; + private static final int PLAYER_HOTBAR_VISIBLE_SLOTS = 9; + private static final int NETWORK_VISIBLE_SLOTS = 60; + private static final int STORAGE_METER_SEGMENTS = 20; + private static final String[] STORAGE_METER_PALETTES = {"Green", "Yellow", "Orange", "Red"}; private static final String ACTION_TAKE = "Take"; + private static final String ACTION_TAKE_HALF = "TakeHalf"; + private static final String ACTION_TAKE_ALL_OF_ITEM = "TakeAllOfItem"; private static final String ACTION_DEPOSIT_PLAYER_SLOT = "DepositPlayerSlot"; private static final String ACTION_TAKE_ALL_TOP = "TakeAllTop"; private static final String ACTION_PULL_ALL_TOP = "PullAllTop"; private static final String ACTION_SORT_NAME = "SortName"; private static final String ACTION_SORT_COUNT = "SortCount"; + private static final String ACTION_PAGE_PREV = "PagePrev"; + private static final String ACTION_PAGE_NEXT = "PageNext"; + private static final String ACTION_HOVER_ENTER = "HoverEnter"; + private static final String ACTION_HOVER_EXIT = "HoverExit"; private static final Map SORT_PREFERENCES = new HashMap<>(); private final Vector3i terminalPosition; @@ -59,6 +69,7 @@ public class TerminalInventoryPage extends InteractiveCustomUIPage slots = resolveSlots(network); + clampPageIndex(slots.size()); + updateSlotLabels(commandBuilder, slots); updatePlayerInventoryGrids(commandBuilder, ref, store); + updateStorageMeter(commandBuilder, network); commandBuilder.set("#SearchInput.Value", searchQuery); - updateControlLabels(commandBuilder); + updateControlLabels(commandBuilder, slots.size()); } /** @@ -108,6 +124,7 @@ public void handleDataEvent( ) { if (data.searchQuery != null) { searchQuery = data.searchQuery.trim().toLowerCase(); + pageIndex = 0; String message = searchQuery.isEmpty() ? "Search cleared." : "Search: " + searchQuery; @@ -138,20 +155,68 @@ public void handleDataEvent( if (ACTION_SORT_NAME.equalsIgnoreCase(data.type)) { applySortSelection(SortField.NAME); + pageIndex = 0; refresh(playerRef, store, "Sort: Name " + (sortDescending ? "descending" : "ascending") + "."); return; } if (ACTION_SORT_COUNT.equalsIgnoreCase(data.type)) { applySortSelection(SortField.COUNT); + pageIndex = 0; refresh(playerRef, store, "Sort: Count " + (sortDescending ? "descending" : "ascending") + "."); return; } + if (ACTION_PAGE_PREV.equalsIgnoreCase(data.type)) { + if (pageIndex > 0) { + pageIndex--; + } + refresh(playerRef, store, null); + return; + } + + if (ACTION_PAGE_NEXT.equalsIgnoreCase(data.type)) { + pageIndex++; + refresh(playerRef, store, null); + return; + } + + if (ACTION_HOVER_ENTER.equalsIgnoreCase(data.type)) { + int pageSlotIndex = parseSlotIndex(resolveSlotPayload(data)); + showNetworkHoverCard(pageSlotIndex); + return; + } + + if (ACTION_HOVER_EXIT.equalsIgnoreCase(data.type)) { + hideNetworkHoverCard(); + return; + } + if (ACTION_TAKE.equalsIgnoreCase(data.type)) { - int slotIndex = parseSlotIndex(resolveSlotPayload(data)); - String status = slotIndex >= 0 - ? withdrawFromSlot(playerRef, store, slotIndex) + int pageSlotIndex = parseSlotIndex(resolveSlotPayload(data)); + int absoluteSlotIndex = resolveAbsoluteSlotIndex(pageSlotIndex); + String status = absoluteSlotIndex >= 0 + ? withdrawFromSlot(playerRef, store, absoluteSlotIndex, false) + : "Invalid slot selection."; + refresh(playerRef, store, status); + return; + } + + if (ACTION_TAKE_HALF.equalsIgnoreCase(data.type)) { + int pageSlotIndex = parseSlotIndex(resolveSlotPayload(data)); + int absoluteSlotIndex = resolveAbsoluteSlotIndex(pageSlotIndex); + String status = absoluteSlotIndex >= 0 + ? withdrawFromSlot(playerRef, store, absoluteSlotIndex, true) + : "Invalid slot selection."; + refresh(playerRef, store, status); + return; + } + + if (ACTION_TAKE_ALL_OF_ITEM.equalsIgnoreCase(data.type)) { + int pageSlotIndex = parseSlotIndex(resolveSlotPayload(data)); + int absoluteSlotIndex = resolveAbsoluteSlotIndex(pageSlotIndex); + String status = absoluteSlotIndex >= 0 + ? withdrawAllOfItemFromSlot(playerRef, store, absoluteSlotIndex) : "Invalid slot selection."; refresh(playerRef, store, status); return; @@ -164,9 +229,33 @@ public void handleDataEvent( * Registers button events for slot and control actions. */ private void bindEvents(UIEventBuilder eventBuilder) { - // NOTE: ItemGrid-level binding currently causes "Failed to apply CustomUI event bindings" - // in this client build. Keep network withdrawal on explicit controls until we switch to - // a selector format the client accepts. + for (int slot = 0; slot < NETWORK_VISIBLE_SLOTS; slot++) { + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#NetworkSlot" + slot, + EventData.of("Type", ACTION_TAKE).append("Slot", Integer.toString(slot)), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.RightClicking, + "#NetworkSlot" + slot, + EventData.of("Type", ACTION_TAKE_HALF).append("Slot", Integer.toString(slot)), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.MouseEntered, + "#NetworkSlot" + slot, + EventData.of("Type", ACTION_HOVER_ENTER).append("Slot", Integer.toString(slot)), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.MouseExited, + "#NetworkSlot" + slot, + EventData.of("Type", ACTION_HOVER_EXIT), + false + ); + } + for (int slot = 0; slot < PLAYER_STORAGE_VISIBLE_SLOTS; slot++) { eventBuilder.addEventBinding( CustomUIEventBindingType.Activating, @@ -208,6 +297,18 @@ private void bindEvents(UIEventBuilder eventBuilder) { EventData.of("Type", ACTION_SORT_COUNT), false ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#NetworkPagePrev", + EventData.of("Type", ACTION_PAGE_PREV), + false + ); + eventBuilder.addEventBinding( + CustomUIEventBindingType.Activating, + "#NetworkPageNext", + EventData.of("Type", ACTION_PAGE_NEXT), + false + ); eventBuilder.addEventBinding( CustomUIEventBindingType.ValueChanged, "#SearchInput", @@ -230,6 +331,16 @@ private int parseSlotIndex(String slot) { } } + /** + * Converts the clicked slot index on the current page to an absolute filtered slot index. + */ + private int resolveAbsoluteSlotIndex(int pageSlotIndex) { + if (pageSlotIndex < 0) { + return -1; + } + return (pageIndex * NETWORK_VISIBLE_SLOTS) + pageSlotIndex; + } + /** * Resolves slot index payload from known UI event key variants. */ @@ -237,8 +348,8 @@ private String resolveSlotPayload(TerminalEventData data) { if (!isNullOrBlank(data.slot)) { return data.slot; } - if (!isNullOrBlank(data.slotIndex)) { - return data.slotIndex; + if (data.slotIndex != null) { + return Integer.toString(data.slotIndex); } if (!isNullOrBlank(data.selectedSlot)) { return data.selectedSlot; @@ -258,20 +369,26 @@ private boolean isNullOrBlank(String value) { */ private void refresh(Ref playerRef, Store store, String status) { UICommandBuilder commands = new UICommandBuilder(); - updateSlotLabels(commands, resolveSlots(resolveNetwork())); + Network network = resolveNetwork(); + List slots = resolveSlots(network); + clampPageIndex(slots.size()); + updateSlotLabels(commands, slots); updatePlayerInventoryGrids(commands, playerRef, store); - updateControlLabels(commands); + updateStorageMeter(commands, network); + updateControlLabels(commands, slots.size()); sendUpdate(commands, null, false); } /** * Updates dynamic control labels. */ - private void updateControlLabels(UICommandBuilder commands) { + private void updateControlLabels(UICommandBuilder commands, int totalNetworkSlots) { String nameOrder = sortField == SortField.NAME ? (sortDescending ? " (desc)" : " (asc)") : ""; String countOrder = sortField == SortField.COUNT ? (sortDescending ? " (desc)" : " (asc)") : ""; commands.set("#SortByName.Text", "Name" + nameOrder); commands.set("#SortByCount.Text", "Count" + countOrder); + int totalPages = Math.max(1, (int) Math.ceil((double) totalNetworkSlots / NETWORK_VISIBLE_SLOTS)); + commands.set("#NetworkPageLabel.Text", (pageIndex + 1) + "/" + totalPages); } /** @@ -298,7 +415,7 @@ private void persistSortPreference() { /** * Withdraws one max-size stack from the selected slot into player inventory. */ - private String withdrawFromSlot(Ref playerRef, Store store, int slotIndex) { + private String withdrawFromSlot(Ref playerRef, Store store, int slotIndex, boolean halfStack) { Network network = resolveNetwork(); if (network == null) { return "Terminal is not connected to a network."; @@ -311,7 +428,8 @@ private String withdrawFromSlot(Ref playerRef, Store s ItemView selected = slots.get(slotIndex); int maxStack = resolveMaxStackSize(selected.itemId); - int amountToWithdraw = Math.min(selected.count, maxStack); + int desired = halfStack ? Math.max(1, (int) Math.ceil(maxStack / 2.0)) : maxStack; + int amountToWithdraw = Math.min(selected.count, desired); if (amountToWithdraw <= 0) { return "Nothing to withdraw."; } @@ -333,6 +451,57 @@ private String withdrawFromSlot(Ref playerRef, Store s return "Withdrew " + amountToWithdraw + " of " + selected.itemId + "."; } + /** + * Withdraws as many items as possible for the selected item id into player inventory. + */ + private String withdrawAllOfItemFromSlot(Ref playerRef, Store store, int slotIndex) { + Network network = resolveNetwork(); + if (network == null) { + return "Terminal is not connected to a network."; + } + + List slots = resolveSlots(network); + if (slotIndex < 0 || slotIndex >= slots.size()) { + return "Slot is empty."; + } + + String itemId = slots.get(slotIndex).itemId; + int totalAvailable = slots.get(slotIndex).count; + if (totalAvailable <= 0) { + return "Nothing to withdraw."; + } + + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + return "Player unavailable."; + } + + ItemContainer destination = player.getInventory().getCombinedEverything(); + int remaining = totalAvailable; + int moved = 0; + while (remaining > 0) { + int maxStack = resolveMaxStackSize(itemId); + int request = Math.min(remaining, maxStack); + ItemStackTransaction tx = destination.addItemStack(new ItemStack(itemId, request)); + ItemStack remainder = tx.getRemainder(); + int remainderCount = (remainder == null || remainder.isEmpty()) ? 0 : remainder.getQuantity(); + int accepted = request - remainderCount; + if (accepted <= 0) { + break; + } + moved += accepted; + remaining -= accepted; + } + + if (moved <= 0) { + return "Inventory is full."; + } + + removeFromNetworkStorage(network, itemId, moved); + StateManager.getInstance().save(); + return "Withdrew " + moved + " of " + itemId + "."; + } + /** * Deposits all stacks from selected inventory sections into network storage. */ @@ -643,12 +812,107 @@ private void writeNetworkTotals(Network network, Map totals) { */ private void updateSlotLabels(UICommandBuilder commands, List slots) { List slotData = new ArrayList<>(); - for (ItemView slot : slots) { + int start = pageIndex * NETWORK_VISIBLE_SLOTS; + int end = Math.min(start + NETWORK_VISIBLE_SLOTS, slots.size()); + for (int i = start; i < end; i++) { + ItemView slot = slots.get(i); slotData.add(new ItemGridSlot(new ItemStack(slot.itemId, slot.count))); } + while (slotData.size() < NETWORK_VISIBLE_SLOTS) { + slotData.add(new ItemGridSlot()); + } + for (int slot = 0; slot < NETWORK_VISIBLE_SLOTS; slot++) { + commands.set("#NetworkSlot" + slot + ".Visible", true); + commands.set("#NetworkHoverCard" + slot + ".Visible", false); + } commands.set("#NetworkGrid.Slots", slotData); } + /** + * Converts item id text into a cleaner title-cased display name. + */ + private String formatDisplayName(String itemId) { + if (itemId == null || itemId.isBlank()) { + return "Unknown Item"; + } + String value = itemId; + int colon = value.lastIndexOf(':'); + if (colon >= 0 && colon < value.length() - 1) { + value = value.substring(colon + 1); + } + int slash = value.lastIndexOf('/'); + if (slash >= 0 && slash < value.length() - 1) { + value = value.substring(slash + 1); + } + value = value.replaceAll("[_\\-.]+", " ").trim(); + if (value.isEmpty()) { + return itemId; + } + String[] words = value.split("\\s+"); + StringBuilder builder = new StringBuilder(); + for (String word : words) { + if (word.isEmpty()) { + continue; + } + if (builder.length() > 0) { + builder.append(' '); + } + builder.append(Character.toUpperCase(word.charAt(0))); + if (word.length() > 1) { + builder.append(word.substring(1).toLowerCase(Locale.US)); + } + } + return builder.toString(); + } + + /** + * Shows the custom hover card for the selected page slot. + */ + private void showNetworkHoverCard(int pageSlotIndex) { + int absoluteSlotIndex = resolveAbsoluteSlotIndex(pageSlotIndex); + Network network = resolveNetwork(); + List slots = resolveSlots(network); + + UICommandBuilder commands = new UICommandBuilder(); + for (int i = 0; i < NETWORK_VISIBLE_SLOTS; i++) { + commands.set("#NetworkHoverCard" + i + ".Visible", false); + } + if (absoluteSlotIndex < 0 || absoluteSlotIndex >= slots.size()) { + sendUpdate(commands, null, false); + return; + } + + ItemView slot = slots.get(absoluteSlotIndex); + commands.set("#NetworkHoverTitle" + pageSlotIndex + ".Text", formatDisplayName(slot.itemId)); + commands.set("#NetworkHoverId" + pageSlotIndex + ".Text", slot.itemId); + commands.set("#NetworkHoverCard" + pageSlotIndex + ".Visible", true); + sendUpdate(commands, null, false); + } + + /** + * Hides the custom network hover card. + */ + private void hideNetworkHoverCard() { + UICommandBuilder commands = new UICommandBuilder(); + for (int i = 0; i < NETWORK_VISIBLE_SLOTS; i++) { + commands.set("#NetworkHoverCard" + i + ".Visible", false); + } + sendUpdate(commands, null, false); + } + + /** + * Keeps page index valid when filters or storage contents change. + */ + private void clampPageIndex(int totalSlots) { + int maxPage = Math.max(0, (int) Math.ceil((double) totalSlots / NETWORK_VISIBLE_SLOTS) - 1); + if (pageIndex > maxPage) { + pageIndex = maxPage; + } + if (pageIndex < 0) { + pageIndex = 0; + } + } + /** * Mirrors the player's storage and hotbar inventories into the custom page. */ @@ -659,12 +923,16 @@ private void updatePlayerInventoryGrids( ) { Player player = store.getComponent(playerRef, Player.getComponentType()); if (player == null) { + List emptyStorage = new ArrayList<>(); for (int i = 0; i < PLAYER_STORAGE_VISIBLE_SLOTS; i++) { - commands.set("#PlayerStorageGrid" + i + ".Slots", new ItemGridSlot[]{new ItemGridSlot()}); + emptyStorage.add(new ItemGridSlot()); } + List emptyHotbar = new ArrayList<>(); for (int i = 0; i < PLAYER_HOTBAR_VISIBLE_SLOTS; i++) { - commands.set("#PlayerHotbarGrid" + i + ".Slots", new ItemGridSlot[]{new ItemGridSlot()}); + emptyHotbar.add(new ItemGridSlot()); } + commands.set("#PlayerStorageGrid.Slots", emptyStorage); + commands.set("#PlayerHotbarGrid.Slots", emptyHotbar); return; } @@ -672,20 +940,81 @@ private void updatePlayerInventoryGrids( ItemContainer storage = inventory.getStorage(); ItemContainer hotbar = inventory.getHotbar(); + List storageData = new ArrayList<>(); for (int i = 0; i < PLAYER_STORAGE_VISIBLE_SLOTS; i++) { ItemStack stack = i < storage.getCapacity() ? storage.getItemStack((short) i) : null; - ItemGridSlot[] data = (stack == null || stack.isEmpty()) - ? new ItemGridSlot[]{new ItemGridSlot()} - : new ItemGridSlot[]{new ItemGridSlot(stack)}; - commands.set("#PlayerStorageGrid" + i + ".Slots", data); + storageData.add((stack == null || stack.isEmpty()) ? new ItemGridSlot() : new ItemGridSlot(stack)); } + commands.set("#PlayerStorageGrid.Slots", storageData); + + List hotbarData = new ArrayList<>(); for (int i = 0; i < PLAYER_HOTBAR_VISIBLE_SLOTS; i++) { ItemStack stack = i < hotbar.getCapacity() ? hotbar.getItemStack((short) i) : null; - ItemGridSlot[] data = (stack == null || stack.isEmpty()) - ? new ItemGridSlot[]{new ItemGridSlot()} - : new ItemGridSlot[]{new ItemGridSlot(stack)}; - commands.set("#PlayerHotbarGrid" + i + ".Slots", data); + hotbarData.add((stack == null || stack.isEmpty()) ? new ItemGridSlot() : new ItemGridSlot(stack)); + } + commands.set("#PlayerHotbarGrid.Slots", hotbarData); + } + + /** + * Updates the side storage usage meter and labels. + */ + private void updateStorageMeter(UICommandBuilder commands, Network network) { + int used = 0; + int max = 0; + if (network != null) { + Map totals = getNetworkTotals(network); + used = totals.values().stream().mapToInt(Integer::intValue).sum(); + max = network.getServerStorages().size() * DeviceServerStorage.getStorageMax(); + } + + double ratio = max > 0 ? Math.min(1.0, (double) used / max) : 0.0; + int percent = (int) Math.round(ratio * 100.0); + int filledSegments = (int) Math.round(ratio * STORAGE_METER_SEGMENTS); + if (used > 0 && filledSegments == 0) { + filledSegments = 1; } + + String usedFormatted = formatWithCommas(used); + String maxFormatted = formatWithCommas(max); + String activePalette = resolveStorageMeterPalette(ratio, used, max); + + commands.set("#StorageMeterPercent.Text", percent + "%"); + commands.set("#StorageMeterUsedGreen.Text", usedFormatted); + commands.set("#StorageMeterUsedYellow.Text", usedFormatted); + commands.set("#StorageMeterUsedOrange.Text", usedFormatted); + commands.set("#StorageMeterUsedRed.Text", usedFormatted); + commands.set("#StorageMeterQuota.Text", "/ " + maxFormatted); + + for (String palette : STORAGE_METER_PALETTES) { + boolean isActive = palette.equals(activePalette); + commands.set("#StorageMeterUsed" + palette + ".Visible", isActive); + for (int i = 0; i < STORAGE_METER_SEGMENTS; i++) { + commands.set("#StorageMeterSeg" + palette + i + ".Visible", isActive && i < filledSegments); + } + } + } + + /** + * Selects the color palette for storage usage visuals. + */ + private String resolveStorageMeterPalette(double ratio, int used, int max) { + if (max > 0 && used >= max) { + return "Red"; + } + if (ratio >= 0.85) { + return "Orange"; + } + if (ratio >= 0.65) { + return "Yellow"; + } + return "Green"; + } + + /** + * Formats numbers with comma thousands separators. + */ + private String formatWithCommas(int value) { + return String.format(Locale.US, "%,d", Math.max(0, value)); } /** @@ -742,7 +1071,7 @@ private SortPreference(SortField field, boolean descending) { public static final class TerminalEventData { private static final String KEY_TYPE = "Type"; private static final String KEY_SLOT = "Slot"; - private static final String KEY_SLOT_INDEX = "SlotIndex"; + private static final String KEY_SLOT_INDEX = "@SlotIndex"; private static final String KEY_SELECTED_SLOT = "SelectedSlot"; private static final String KEY_SEARCH_QUERY = "@SearchQuery"; @@ -759,7 +1088,7 @@ public static final class TerminalEventData { data -> data.slot ).add() .append( - new KeyedCodec<>(KEY_SLOT_INDEX, Codec.STRING), + new KeyedCodec<>(KEY_SLOT_INDEX, Codec.INTEGER), (data, value) -> data.slotIndex = value, data -> data.slotIndex ).add() @@ -777,7 +1106,7 @@ public static final class TerminalEventData { private String type; private String slot; - private String slotIndex; + private Integer slotIndex; private String selectedSlot; private String searchQuery; diff --git a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui index 6bfb8df..cb1b083 100644 --- a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui +++ b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui @@ -2,7 +2,8 @@ $C = "../Common.ui"; @SlotSize = 72; @SlotGap = 8; -@GridWidth = 792; +@NetworkGridWidth = 952; +@PlayerGridWidth = 712; Group #TerminalRoot { Anchor: (Full: 0); @@ -12,123 +13,1008 @@ Group #TerminalRoot { LayoutMode: Center; $C.@Container #TerminalWindow { - Anchor: (Width: 1120, Height: 1100); + Anchor: (Width: 1180, Height: 1070); #Title { Group { - LayoutMode: Full; - Anchor: (Height: 24); - Group { - Anchor: (Top: 0, Height: 24, Left: 10, Width: 250); - LayoutMode: Left; - $C.@SmallSecondaryTextButton #SortByName { - Anchor: (Width: 120, Height: 24); - Text: "Name"; - } - $C.@SmallSecondaryTextButton #SortByCount { - Anchor: (Width: 120, Height: 24, Left: 10); - Text: "Count"; - } + LayoutMode: Full; + Anchor: (Height: 24); + Group { + Anchor: (Top: 0, Height: 24, Left: 10, Width: 300); + LayoutMode: Left; + $C.@SmallSecondaryTextButton #SortByName { + Anchor: (Width: 136, Height: 24); + Text: "Name"; } - Group { - Anchor: (Top: 0, Height: 24, Right: 10, Width: 94); - LayoutMode: Left; - $C.@SmallSecondaryTextButton #TakeAllTop { - Anchor: (Width: 44, Height: 24); - Text: "<"; - TooltipText: "Take all - Move all items from the terminal to your inventory. (Q)"; - TextTooltipStyle: $C.@DefaultTextTooltipStyle; - } - $C.@SmallSecondaryTextButton #PullAllTop { - Anchor: (Width: 44, Height: 24, Left: 6); - Text: ">"; - TooltipText: "Pull all - Move all items from your inventory to the terminal. (E)"; - TextTooltipStyle: $C.@DefaultTextTooltipStyle; - } + $C.@SmallSecondaryTextButton #SortByCount { + Anchor: (Width: 136, Height: 24, Left: 10); + Text: "Count"; + } + } + Group { + Anchor: (Top: 0, Height: 24, Right: 10, Width: 390); + LayoutMode: Left; + $C.@SmallSecondaryTextButton #NetworkPagePrev { + Anchor: (Width: 30, Height: 24); + Text: "<"; + TooltipText: "Previous page"; + TextTooltipStyle: $C.@DefaultTextTooltipStyle; + } + Label #NetworkPageLabel { + Anchor: (Width: 48, Height: 24, Left: 6); + Text: "1/1"; + Style: (...$C.@DefaultLabelStyle, HorizontalAlignment: Center, RenderBold: true); + } + $C.@SmallSecondaryTextButton #NetworkPageNext { + Anchor: (Width: 30, Height: 24, Left: 6); + Text: ">"; + TooltipText: "Next page"; + TextTooltipStyle: $C.@DefaultTextTooltipStyle; + } + $C.@SmallSecondaryTextButton #TakeAllTop { + Anchor: (Width: 130, Height: 24, Left: 10); + Text: "Take All"; + TooltipText: "Take all - Move all items from the terminal to your inventory. (Q)"; + TextTooltipStyle: $C.@DefaultTextTooltipStyle; + } + $C.@SmallSecondaryTextButton #PullAllTop { + Anchor: (Width: 130, Height: 24, Left: 6); + Text: "Put All"; + TooltipText: "Pull all - Move all items from your inventory to the terminal. (E)"; + TextTooltipStyle: $C.@DefaultTextTooltipStyle; } } } + } #Content { Group { LayoutMode: Top; - Anchor: (Top: 0, Bottom: 10); + Anchor: (Top: -5, Bottom: 10); - Group { - Anchor: (Height: 56); - LayoutMode: Top; - Label { - Anchor: (Height: 18); - Text: "Search Item Id"; - Style: (...$C.@DefaultLabelStyle, FontSize: 13, RenderUppercase: true); - } - Group { - Anchor: (Height: 34, Top: 4); - Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); - TextField #SearchInput { - Anchor: (Horizontal: 8, Vertical: 5); - Value: ""; + Group { + Anchor: (Height: 56); + LayoutMode: Top; + Label { + Anchor: (Height: 18); + Text: "Search Item Id"; + Style: (...$C.@DefaultLabelStyle, FontSize: 13, RenderUppercase: true); + } + Group { + Anchor: (Height: 34, Top: 4); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + TextField #SearchInput { + Anchor: (Horizontal: 8, Vertical: 5); + Value: ""; + } + } } - } - } - - Label { - Anchor: (Top: 10, Height: 20); - Text: "NETWORK INVENTORY"; - Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); - } - Group { - Anchor: (Top: 6, Height: 444); - Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); - Group #NetworkScroll { - Anchor: (Horizontal: 6, Vertical: 6); - LayoutMode: TopScrolling; - ScrollbarStyle: $C.@DefaultScrollbarStyle; - ItemGrid #NetworkGrid { - Anchor: (Width: @GridWidth, Height: 4000); - SlotsPerRow: 10; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: @SlotGap, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); + Label { + Anchor: (Top: 10, Height: 20); + Text: "NETWORK INVENTORY"; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); } - } - } - - Label { - Anchor: (Top: 45, Height: 20); - Text: "INVENTORY"; - Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); - } - Group { - Anchor: (Top: 10, Height: 250); - Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); - LayoutMode: Center; - Group { - Anchor: (Width: @GridWidth, Height: 232); - LayoutMode: Top; Group { - Anchor: (Height: @SlotSize, Top: 0); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + Anchor: (Top: 6, Height: 444); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + LayoutMode: Center; + Group #NetworkScroll { + Anchor: (Width: @NetworkGridWidth, Height: 392); LayoutMode: Full; - ItemGrid #PlayerStorageGrid0 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + ItemGrid #NetworkGrid { + Anchor: (Width: @NetworkGridWidth, Height: 392); + SlotsPerRow: 12; Style: ( SlotSize: @SlotSize, SlotIconSize: @SlotSize, - SlotSpacing: 0, + SlotSpacing: @SlotGap, SlotBackground: "../Common/BlockSelectorSlotBackground.png" ); } - Button #PlayerStorageSlot0 { - Anchor: (Full: 0); + Group #NetworkHoverCard0 { + Anchor: (Left: 0, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle0 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId0 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard1 { + Anchor: (Left: 80, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle1 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId1 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard2 { + Anchor: (Left: 160, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle2 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId2 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard3 { + Anchor: (Left: 240, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle3 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId3 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard4 { + Anchor: (Left: 320, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle4 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId4 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard5 { + Anchor: (Left: 400, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle5 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId5 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard6 { + Anchor: (Left: 480, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle6 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId6 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard7 { + Anchor: (Left: 560, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle7 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId7 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard8 { + Anchor: (Left: 640, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle8 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId8 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard9 { + Anchor: (Left: 720, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle9 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId9 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard10 { + Anchor: (Left: 800, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle10 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId10 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard11 { + Anchor: (Left: 880, Top: 82, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle11 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId11 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard12 { + Anchor: (Left: 0, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle12 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId12 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard13 { + Anchor: (Left: 80, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle13 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId13 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard14 { + Anchor: (Left: 160, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle14 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId14 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard15 { + Anchor: (Left: 240, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle15 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId15 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard16 { + Anchor: (Left: 320, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle16 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId16 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard17 { + Anchor: (Left: 400, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle17 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId17 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard18 { + Anchor: (Left: 480, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle18 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId18 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard19 { + Anchor: (Left: 560, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle19 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId19 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard20 { + Anchor: (Left: 640, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle20 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId20 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard21 { + Anchor: (Left: 720, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle21 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId21 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard22 { + Anchor: (Left: 800, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle22 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId22 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard23 { + Anchor: (Left: 880, Top: 14, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle23 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId23 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard24 { + Anchor: (Left: 0, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle24 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId24 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard25 { + Anchor: (Left: 80, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle25 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId25 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard26 { + Anchor: (Left: 160, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle26 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId26 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard27 { + Anchor: (Left: 240, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle27 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId27 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard28 { + Anchor: (Left: 320, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle28 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId28 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard29 { + Anchor: (Left: 400, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle29 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId29 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard30 { + Anchor: (Left: 480, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle30 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId30 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard31 { + Anchor: (Left: 560, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle31 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId31 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard32 { + Anchor: (Left: 640, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle32 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId32 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard33 { + Anchor: (Left: 720, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle33 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId33 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard34 { + Anchor: (Left: 800, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle34 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId34 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard35 { + Anchor: (Left: 880, Top: 94, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle35 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId35 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard36 { + Anchor: (Left: 0, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle36 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId36 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard37 { + Anchor: (Left: 80, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle37 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId37 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard38 { + Anchor: (Left: 160, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle38 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId38 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard39 { + Anchor: (Left: 240, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle39 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId39 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard40 { + Anchor: (Left: 320, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle40 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId40 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard41 { + Anchor: (Left: 400, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle41 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId41 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard42 { + Anchor: (Left: 480, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle42 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId42 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard43 { + Anchor: (Left: 560, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle43 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId43 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard44 { + Anchor: (Left: 640, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle44 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId44 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard45 { + Anchor: (Left: 720, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle45 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId45 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard46 { + Anchor: (Left: 800, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle46 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId46 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard47 { + Anchor: (Left: 880, Top: 174, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle47 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId47 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard48 { + Anchor: (Left: 0, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle48 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId48 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard49 { + Anchor: (Left: 80, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle49 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId49 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard50 { + Anchor: (Left: 160, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle50 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId50 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard51 { + Anchor: (Left: 240, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle51 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId51 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard52 { + Anchor: (Left: 320, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle52 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId52 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard53 { + Anchor: (Left: 400, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle53 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId53 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard54 { + Anchor: (Left: 480, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle54 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId54 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard55 { + Anchor: (Left: 560, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle55 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId55 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard56 { + Anchor: (Left: 640, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle56 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId56 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard57 { + Anchor: (Left: 720, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle57 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId57 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard58 { + Anchor: (Left: 800, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle58 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId58 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Group #NetworkHoverCard59 { + Anchor: (Left: 880, Top: 254, Width: 260, Height: 64); + Visible: false; + Background: #0f1b2eff; + Label #NetworkHoverTitle59 { + Anchor: (Top: 6, Left: 8, Right: 8, Height: 26); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 17, HorizontalAlignment: Center, RenderBold: true, TextColor: #f2f6ff); + } + Label #NetworkHoverId59 { + Anchor: (Top: 34, Left: 8, Right: 8, Height: 18); + Text: ""; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #8da0b8); + } + } + Button #NetworkSlot0 { + Anchor: (Left: 0, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -136,22 +1022,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid1 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot1 { + Anchor: (Left: 80, Top: 0, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot1 { - Anchor: (Full: 0); + Button #NetworkSlot2 { + Anchor: (Left: 160, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -159,22 +1040,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid2 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot3 { + Anchor: (Left: 240, Top: 0, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot2 { - Anchor: (Full: 0); + Button #NetworkSlot4 { + Anchor: (Left: 320, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -182,22 +1058,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid3 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot5 { + Anchor: (Left: 400, Top: 0, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot3 { - Anchor: (Full: 0); + Button #NetworkSlot6 { + Anchor: (Left: 480, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -205,22 +1076,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid4 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot7 { + Anchor: (Left: 560, Top: 0, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot4 { - Anchor: (Full: 0); + Button #NetworkSlot8 { + Anchor: (Left: 640, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -228,22 +1094,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid5 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot9 { + Anchor: (Left: 720, Top: 0, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot5 { - Anchor: (Full: 0); + Button #NetworkSlot10 { + Anchor: (Left: 800, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -251,22 +1112,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid6 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot11 { + Anchor: (Left: 880, Top: 0, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot6 { - Anchor: (Full: 0); + Button #NetworkSlot12 { + Anchor: (Left: 0, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -274,22 +1130,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid7 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot13 { + Anchor: (Left: 80, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot7 { - Anchor: (Full: 0); + Button #NetworkSlot14 { + Anchor: (Left: 160, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -297,22 +1148,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid8 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot15 { + Anchor: (Left: 240, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot8 { - Anchor: (Full: 0); + Button #NetworkSlot16 { + Anchor: (Left: 320, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -320,22 +1166,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid9 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot17 { + Anchor: (Left: 400, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot9 { - Anchor: (Full: 0); + Button #NetworkSlot18 { + Anchor: (Left: 480, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -343,26 +1184,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - } - Group { - Anchor: (Height: @SlotSize, Top: 6); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid10 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot19 { + Anchor: (Left: 560, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot10 { - Anchor: (Full: 0); + Button #NetworkSlot20 { + Anchor: (Left: 640, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -370,22 +1202,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid11 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot21 { + Anchor: (Left: 720, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot11 { - Anchor: (Full: 0); + Button #NetworkSlot22 { + Anchor: (Left: 800, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -393,22 +1220,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid12 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot23 { + Anchor: (Left: 880, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot12 { - Anchor: (Full: 0); + Button #NetworkSlot24 { + Anchor: (Left: 0, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -416,22 +1238,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid13 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot25 { + Anchor: (Left: 80, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot13 { - Anchor: (Full: 0); + Button #NetworkSlot26 { + Anchor: (Left: 160, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -439,22 +1256,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid14 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot27 { + Anchor: (Left: 240, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot14 { - Anchor: (Full: 0); + Button #NetworkSlot28 { + Anchor: (Left: 320, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -462,22 +1274,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid15 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot29 { + Anchor: (Left: 400, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot15 { - Anchor: (Full: 0); + Button #NetworkSlot30 { + Anchor: (Left: 480, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -485,22 +1292,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid16 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot31 { + Anchor: (Left: 560, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot16 { - Anchor: (Full: 0); + Button #NetworkSlot32 { + Anchor: (Left: 640, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -508,22 +1310,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid17 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot33 { + Anchor: (Left: 720, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot17 { - Anchor: (Full: 0); + Button #NetworkSlot34 { + Anchor: (Left: 800, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -531,22 +1328,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid18 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot35 { + Anchor: (Left: 880, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot18 { - Anchor: (Full: 0); + Button #NetworkSlot36 { + Anchor: (Left: 0, Top: 240, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -554,22 +1346,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid19 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #NetworkSlot37 { + Anchor: (Left: 80, Top: 240, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot19 { - Anchor: (Full: 0); + Button #NetworkSlot38 { + Anchor: (Left: 160, Top: 240, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -577,26 +1364,367 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - } - Group { - Anchor: (Height: @SlotSize, Top: 6); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); + Button #NetworkSlot39 { + Anchor: (Left: 240, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot40 { + Anchor: (Left: 320, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot41 { + Anchor: (Left: 400, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot42 { + Anchor: (Left: 480, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot43 { + Anchor: (Left: 560, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot44 { + Anchor: (Left: 640, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot45 { + Anchor: (Left: 720, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot46 { + Anchor: (Left: 800, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot47 { + Anchor: (Left: 880, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot48 { + Anchor: (Left: 0, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot49 { + Anchor: (Left: 80, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot50 { + Anchor: (Left: 160, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot51 { + Anchor: (Left: 240, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot52 { + Anchor: (Left: 320, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot53 { + Anchor: (Left: 400, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot54 { + Anchor: (Left: 480, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot55 { + Anchor: (Left: 560, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot56 { + Anchor: (Left: 640, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot57 { + Anchor: (Left: 720, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot58 { + Anchor: (Left: 800, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #NetworkSlot59 { + Anchor: (Left: 880, Top: 320, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + } + } + + Label { + Anchor: (Top: 13, Height: 20); + Text: "INVENTORY"; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, RenderUppercase: true, RenderBold: true); + } + + Group { + Anchor: (Top: 6, Height: 330); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + LayoutMode: Center; + Group { + Anchor: (Width: @PlayerGridWidth, Height: 312); LayoutMode: Full; - ItemGrid #PlayerStorageGrid20 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + ItemGrid #PlayerStorageGrid { + Anchor: (Width: @PlayerGridWidth, Height: 312); + SlotsPerRow: 9; + Style: ( + SlotSize: @SlotSize, + SlotIconSize: @SlotSize, + SlotSpacing: @SlotGap, + SlotBackground: "../Common/BlockSelectorSlotBackground.png" + ); + } + Button #PlayerStorageSlot0 { + Anchor: (Left: 0, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot1 { + Anchor: (Left: 80, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot2 { + Anchor: (Left: 160, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot3 { + Anchor: (Left: 240, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot4 { + Anchor: (Left: 320, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot5 { + Anchor: (Left: 400, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot6 { + Anchor: (Left: 480, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot7 { + Anchor: (Left: 560, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot8 { + Anchor: (Left: 640, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot9 { + Anchor: (Left: 0, Top: 80, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot10 { + Anchor: (Left: 80, Top: 80, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot11 { + Anchor: (Left: 160, Top: 80, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot12 { + Anchor: (Left: 240, Top: 80, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot13 { + Anchor: (Left: 320, Top: 80, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot14 { + Anchor: (Left: 400, Top: 80, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot15 { + Anchor: (Left: 480, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot20 { - Anchor: (Full: 0); + Button #PlayerStorageSlot16 { + Anchor: (Left: 560, Top: 80, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -604,22 +1732,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid21 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot17 { + Anchor: (Left: 640, Top: 80, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot21 { - Anchor: (Full: 0); + Button #PlayerStorageSlot18 { + Anchor: (Left: 0, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -627,22 +1750,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid22 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot19 { + Anchor: (Left: 80, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot22 { - Anchor: (Full: 0); + Button #PlayerStorageSlot20 { + Anchor: (Left: 160, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -650,22 +1768,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid23 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot21 { + Anchor: (Left: 240, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot23 { - Anchor: (Full: 0); + Button #PlayerStorageSlot22 { + Anchor: (Left: 320, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -673,22 +1786,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid24 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot23 { + Anchor: (Left: 400, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } Button #PlayerStorageSlot24 { - Anchor: (Full: 0); + Anchor: (Left: 480, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -696,22 +1804,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid25 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot25 { + Anchor: (Left: 560, Top: 160, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot25 { - Anchor: (Full: 0); + Button #PlayerStorageSlot26 { + Anchor: (Left: 640, Top: 160, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -719,22 +1822,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid26 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot27 { + Anchor: (Left: 0, Top: 240, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot26 { - Anchor: (Full: 0); + Button #PlayerStorageSlot28 { + Anchor: (Left: 80, Top: 240, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -742,22 +1840,17 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid27 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot29 { + Anchor: (Left: 160, Top: 240, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot27 { - Anchor: (Full: 0); + Button #PlayerStorageSlot30 { + Anchor: (Left: 240, Top: 240, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -765,22 +1858,44 @@ Group #TerminalRoot { Disabled: (Background: #00000000) ); } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerStorageGrid28 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + Button #PlayerStorageSlot31 { + Anchor: (Left: 320, Top: 240, Width: 72, Height: 72); Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) ); } - Button #PlayerStorageSlot28 { - Anchor: (Full: 0); + Button #PlayerStorageSlot32 { + Anchor: (Left: 400, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot33 { + Anchor: (Left: 480, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot34 { + Anchor: (Left: 560, Top: 240, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerStorageSlot35 { + Anchor: (Left: 640, Top: 240, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -789,21 +1904,99 @@ Group #TerminalRoot { ); } } + } + + Group { + Anchor: (Top: 8, Height: 88); + Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); + LayoutMode: Center; Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); + Anchor: (Width: @PlayerGridWidth, Height: 72); LayoutMode: Full; - ItemGrid #PlayerStorageGrid29 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; + ItemGrid #PlayerHotbarGrid { + Anchor: (Width: @PlayerGridWidth, Height: 72); + SlotsPerRow: 9; Style: ( SlotSize: @SlotSize, SlotIconSize: @SlotSize, - SlotSpacing: 0, + SlotSpacing: @SlotGap, SlotBackground: "../Common/BlockSelectorSlotBackground.png" ); } - Button #PlayerStorageSlot29 { - Anchor: (Full: 0); + Button #PlayerHotbarSlot0 { + Anchor: (Left: 0, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot1 { + Anchor: (Left: 80, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot2 { + Anchor: (Left: 160, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot3 { + Anchor: (Left: 240, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot4 { + Anchor: (Left: 320, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot5 { + Anchor: (Left: 400, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot6 { + Anchor: (Left: 480, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot7 { + Anchor: (Left: 560, Top: 0, Width: 72, Height: 72); + Style: ( + Default: (Background: #00000000), + Hovered: (Background: #cfe1ff22), + Pressed: (Background: #cfe1ff44), + Disabled: (Background: #00000000) + ); + } + Button #PlayerHotbarSlot8 { + Anchor: (Left: 640, Top: 0, Width: 72, Height: 72); Style: ( Default: (Background: #00000000), Hovered: (Background: #cfe1ff22), @@ -813,251 +2006,149 @@ Group #TerminalRoot { } } } + } } - Group { - Anchor: (Top: 12, Height: 88); + Group #StorageMeterPanel { + Anchor: (Right: -78, Top: 146, Width: 68, Height: 430); Background: (TexturePath: "../Common/ContainerPanelPatch.png", Border: 4); - LayoutMode: Center; - Group { - Anchor: (Width: @GridWidth, Height: @SlotSize); - LayoutMode: Left; - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: 0); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid0 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot0 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid1 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot1 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid2 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot2 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid3 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot3 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid4 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot4 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid5 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot5 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid6 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot6 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid7 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot7 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid8 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot8 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } - Group { - Anchor: (Width: @SlotSize, Height: @SlotSize, Left: @SlotGap); - LayoutMode: Full; - ItemGrid #PlayerHotbarGrid9 { - Anchor: (Width: @SlotSize, Height: @SlotSize); - SlotsPerRow: 1; - Style: ( - SlotSize: @SlotSize, - SlotIconSize: @SlotSize, - SlotSpacing: 0, - SlotBackground: "../Common/BlockSelectorSlotBackground.png" - ); - } - Button #PlayerHotbarSlot9 { - Anchor: (Full: 0); - Style: ( - Default: (Background: #00000000), - Hovered: (Background: #cfe1ff22), - Pressed: (Background: #cfe1ff44), - Disabled: (Background: #00000000) - ); - } - } + + Label { + Anchor: (Top: 8, Left: 0, Right: 0, Height: 14); + Text: "USED"; + Style: (...$C.@DefaultLabelStyle, FontSize: 11, RenderUppercase: true, HorizontalAlignment: Center, RenderBold: true); } - } + Label #StorageMeterUsedGreen { + Anchor: (Top: 23, Left: 0, Right: 0, Height: 18); + Text: "0"; + Visible: true; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, HorizontalAlignment: Center, RenderBold: true, TextColor: #4bb26b); + } + + Label #StorageMeterUsedYellow { + Anchor: (Top: 23, Left: 0, Right: 0, Height: 18); + Text: "0"; + Visible: false; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, HorizontalAlignment: Center, RenderBold: true, TextColor: #d2b13e); + } + + Label #StorageMeterUsedOrange { + Anchor: (Top: 23, Left: 0, Right: 0, Height: 18); + Text: "0"; + Visible: false; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, HorizontalAlignment: Center, RenderBold: true, TextColor: #de8b2f); + } + + Label #StorageMeterUsedRed { + Anchor: (Top: 23, Left: 0, Right: 0, Height: 18); + Text: "0"; + Visible: false; + Style: (...$C.@DefaultLabelStyle, FontSize: 14, HorizontalAlignment: Center, RenderBold: true, TextColor: #c84c4c); + } + + Label #StorageMeterQuota { + Anchor: (Top: 40, Left: 0, Right: 0, Height: 16); + Text: "/ 0"; + Style: (...$C.@DefaultLabelStyle, FontSize: 10, HorizontalAlignment: Center, TextColor: #9fb2c7); + } + + Group #StorageMeterTrack { + Anchor: (Top: 62, Left: 11, Right: 11, Bottom: 30); + Background: (TexturePath: "../Common/InputBox.png", Border: 4); + + Group #StorageMeterSegGreen0 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 0); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen1 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 16); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen2 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 32); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen3 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 48); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen4 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 64); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen5 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 80); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen6 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 96); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen7 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 112); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen8 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 128); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen9 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 144); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen10 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 160); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen11 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 176); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen12 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 192); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen13 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 208); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen14 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 224); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen15 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 240); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen16 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 256); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen17 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 272); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen18 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 288); Background: #4bb26b; Visible: false; } + Group #StorageMeterSegGreen19 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 304); Background: #4bb26b; Visible: false; } + + Group #StorageMeterSegYellow0 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 0); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow1 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 16); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow2 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 32); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow3 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 48); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow4 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 64); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow5 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 80); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow6 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 96); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow7 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 112); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow8 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 128); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow9 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 144); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow10 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 160); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow11 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 176); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow12 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 192); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow13 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 208); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow14 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 224); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow15 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 240); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow16 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 256); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow17 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 272); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow18 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 288); Background: #d2b13e; Visible: false; } + Group #StorageMeterSegYellow19 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 304); Background: #d2b13e; Visible: false; } + + Group #StorageMeterSegOrange0 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 0); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange1 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 16); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange2 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 32); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange3 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 48); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange4 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 64); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange5 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 80); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange6 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 96); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange7 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 112); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange8 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 128); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange9 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 144); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange10 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 160); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange11 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 176); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange12 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 192); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange13 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 208); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange14 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 224); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange15 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 240); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange16 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 256); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange17 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 272); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange18 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 288); Background: #de8b2f; Visible: false; } + Group #StorageMeterSegOrange19 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 304); Background: #de8b2f; Visible: false; } + + Group #StorageMeterSegRed0 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 0); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed1 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 16); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed2 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 32); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed3 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 48); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed4 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 64); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed5 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 80); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed6 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 96); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed7 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 112); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed8 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 128); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed9 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 144); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed10 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 160); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed11 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 176); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed12 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 192); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed13 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 208); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed14 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 224); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed15 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 240); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed16 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 256); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed17 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 272); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed18 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 288); Background: #c84c4c; Visible: false; } + Group #StorageMeterSegRed19 { Anchor: (Left: 4, Right: 4, Height: 14, Bottom: 304); Background: #c84c4c; Visible: false; } + } + + Label #StorageMeterPercent { + Anchor: (Bottom: 8, Left: 0, Right: 0, Height: 16); + Text: "0%"; + Style: (...$C.@DefaultLabelStyle, FontSize: 12, HorizontalAlignment: Center, RenderBold: true, TextColor: #a9bdd3); } - } } } } +} From fdaed365b4ed7f036b4feb40e830a69bd2ec71a5 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 09:46:40 -0500 Subject: [PATCH 04/12] UI changes --- .../hytech/storage/ui/TerminalInventoryPage.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index cb77b67..ada38db 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -51,7 +51,7 @@ public class TerminalInventoryPage extends InteractiveCustomUIPage= 0 @@ -239,7 +239,7 @@ private void bindEvents(UIEventBuilder eventBuilder) { eventBuilder.addEventBinding( CustomUIEventBindingType.RightClicking, "#NetworkSlot" + slot, - EventData.of("Type", ACTION_TAKE_HALF).append("Slot", Integer.toString(slot)), + EventData.of("Type", ACTION_TAKE_ONE).append("Slot", Integer.toString(slot)), false ); eventBuilder.addEventBinding( @@ -415,7 +415,7 @@ private void persistSortPreference() { /** * Withdraws one max-size stack from the selected slot into player inventory. */ - private String withdrawFromSlot(Ref playerRef, Store store, int slotIndex, boolean halfStack) { + private String withdrawFromSlot(Ref playerRef, Store store, int slotIndex, boolean singleItem) { Network network = resolveNetwork(); if (network == null) { return "Terminal is not connected to a network."; @@ -428,7 +428,7 @@ private String withdrawFromSlot(Ref playerRef, Store s ItemView selected = slots.get(slotIndex); int maxStack = resolveMaxStackSize(selected.itemId); - int desired = halfStack ? Math.max(1, (int) Math.ceil(maxStack / 2.0)) : maxStack; + int desired = singleItem ? 1 : maxStack; int amountToWithdraw = Math.min(selected.count, desired); if (amountToWithdraw <= 0) { return "Nothing to withdraw."; From fb95d7b55dba0f40bd9d2bb8ad43d65fb1761fdc Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 13:53:07 -0500 Subject: [PATCH 05/12] Separate storage devices from other devices in save file and codecs. Rename some classes to be more consistent. New storage device codec inherits base device codec. --- .../hytech/storage/network/Network.java | 13 +++++ .../storage/network/SNetworkManager.java | 58 +++++++++---------- .../storage/network/device/DeviceBase.java | 6 +- .../network/device/DeviceServerRack.java | 2 +- .../network/device/DeviceServerStorage.java | 2 +- .../network/device/DeviceTerminal.java | 2 +- ...torageDeviceType.java => EDeviceType.java} | 8 +-- .../hytech/storage/state/StateManager.java | 44 ++++++++------ .../storage/state/codec/DeviceBaseCodec.java | 47 +++++++++++++++ ...viceCodec.java => DeviceStorageCodec.java} | 53 ++++++----------- .../storage/state/codec/NetworkCodec.java | 29 ++++++++-- .../state/codec/NetworkManagerCodec.java | 13 ++++- .../world/events/BlockBreakEventSystem.java | 11 +++- .../events/BlockPlacementEventSystem.java | 5 +- 14 files changed, 187 insertions(+), 106 deletions(-) rename src/main/java/com/github/hytech/storage/network/device/{EStorageDeviceType.java => EDeviceType.java} (82%) create mode 100644 src/main/java/com/github/hytech/storage/state/codec/DeviceBaseCodec.java rename src/main/java/com/github/hytech/storage/state/codec/{DeviceCodec.java => DeviceStorageCodec.java} (52%) diff --git a/src/main/java/com/github/hytech/storage/network/Network.java b/src/main/java/com/github/hytech/storage/network/Network.java index 7642120..d96d32a 100644 --- a/src/main/java/com/github/hytech/storage/network/Network.java +++ b/src/main/java/com/github/hytech/storage/network/Network.java @@ -45,6 +45,19 @@ public Map getDevices() { return devices; } + /** + * Returns all devices in this network, keyed by position. + * + * @return map of device positions to devices + */ + public Map getBaseDevices() { + Map baseDevices = new LinkedHashMap<>(); + baseDevices.putAll(serverRacks); + baseDevices.putAll(terminals); + + return baseDevices; + } + /** * Returns server storage blocks in insertion order. * diff --git a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java index cff24ad..e1291ef 100644 --- a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java +++ b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java @@ -2,11 +2,7 @@ import com.github.hytech.storage.network.device.*; import com.github.hytech.storage.state.StateManager; -import com.github.hytech.storage.state.codec.DeviceCodec; -import com.github.hytech.storage.state.codec.NetworkCodec; -import com.github.hytech.storage.state.codec.NetworkManagerCodec; -import com.github.hytech.storage.state.codec.PositionCodec; -import com.github.hytech.storage.state.codec.StorageItemCodec; +import com.github.hytech.storage.state.codec.*; import com.github.hytech.storage.utility.Logger; import com.github.hytech.storage.world.DetectNeighbors; import com.hypixel.hytale.math.vector.Vector3i; @@ -91,7 +87,7 @@ public Map getTerminals() { * @param deviceType device type being placed * @param position block position */ - public void onDevicePlaced(EStorageDeviceType deviceType, Vector3i position) { + public void onDevicePlaced(EDeviceType deviceType, Vector3i position) { List neighbors = DetectNeighbors.detectNeighbor(position, devices); Network network; @@ -244,7 +240,7 @@ public void restoreIntoNetworkManager(NetworkManagerCodec networkManagerCodec) { * @param networkCodec saved network data */ public void restoreIntoNetwork(NetworkCodec networkCodec) { - DeviceCodec[] deviceCodecs = networkCodec.deviceCodecs; + DeviceBaseCodec[] deviceCodecs = networkCodec.deviceCodecs; if (deviceCodecs == null || deviceCodecs.length == 0) { return; } @@ -252,7 +248,7 @@ public void restoreIntoNetwork(NetworkCodec networkCodec) { UUID networkId = UUID.randomUUID(); Network network = new Network(networkId); - for (DeviceCodec deviceCodec : deviceCodecs) { + for (DeviceBaseCodec deviceCodec : deviceCodecs) { restoreIntoDevice(network, deviceCodec); } @@ -268,30 +264,32 @@ public void restoreIntoNetwork(NetworkCodec networkCodec) { * @param network target network * @param deviceCodec saved device data */ - public void restoreIntoDevice(Network network, DeviceCodec deviceCodec) { + public void restoreIntoDevice(Network network, DeviceBaseCodec deviceCodec) { Vector3i position = restoreIntoPosition(deviceCodec.position); - switch (EStorageDeviceType.fromId(deviceCodec.type)) { - case TERMINAL: - DeviceTerminal terminal = new DeviceTerminal(position, network); - terminals.put(position, terminal); - devices.put(position, terminal); - network.addDevice(terminal); - break; - case SERVER_STORAGE: - DeviceServerStorage serverStorage = new DeviceServerStorage(position, network); - applyStorageItems(serverStorage, deviceCodec.storageItems); - serverStorages.put(position, serverStorage); - devices.put(position, serverStorage); - network.addDevice(serverStorage); - break; - case SERVER_RACK: - case null: - default: - DeviceServerRack serverRack = new DeviceServerRack(position, network); - serverRacks.put(position, serverRack); - devices.put(position, serverRack); - network.addDevice(serverRack); + if (deviceCodec instanceof DeviceStorageCodec deviceStorageCodec) { + DeviceServerStorage serverStorage = new DeviceServerStorage(position, network); + applyStorageItems(serverStorage, deviceStorageCodec.storageItems); + serverStorages.put(position, serverStorage); + devices.put(position, serverStorage); + network.addDevice(serverStorage); + } + else { + switch (EDeviceType.fromId(deviceCodec.type)) { + case TERMINAL: + DeviceTerminal terminal = new DeviceTerminal(position, network); + terminals.put(position, terminal); + devices.put(position, terminal); + network.addDevice(terminal); + break; + case SERVER_RACK: + case null: + default: + DeviceServerRack serverRack = new DeviceServerRack(position, network); + serverRacks.put(position, serverRack); + devices.put(position, serverRack); + network.addDevice(serverRack); + } } } diff --git a/src/main/java/com/github/hytech/storage/network/device/DeviceBase.java b/src/main/java/com/github/hytech/storage/network/device/DeviceBase.java index ab681fd..70896c1 100644 --- a/src/main/java/com/github/hytech/storage/network/device/DeviceBase.java +++ b/src/main/java/com/github/hytech/storage/network/device/DeviceBase.java @@ -8,7 +8,7 @@ */ public class DeviceBase { private final Vector3i position; - private final EStorageDeviceType type; + private final EDeviceType type; private Network network; @@ -19,7 +19,7 @@ public class DeviceBase { * @param type device type * @param network owning network */ - public DeviceBase(Vector3i position, EStorageDeviceType type, Network network) { + public DeviceBase(Vector3i position, EDeviceType type, Network network) { this.position = position; this.type = type; this.network = network; @@ -39,7 +39,7 @@ public Vector3i getPosition() { * * @return storage device type */ - public EStorageDeviceType getType() { return type; } + public EDeviceType getType() { return type; } /** * Updates the network association. diff --git a/src/main/java/com/github/hytech/storage/network/device/DeviceServerRack.java b/src/main/java/com/github/hytech/storage/network/device/DeviceServerRack.java index e749390..b2ff524 100644 --- a/src/main/java/com/github/hytech/storage/network/device/DeviceServerRack.java +++ b/src/main/java/com/github/hytech/storage/network/device/DeviceServerRack.java @@ -14,6 +14,6 @@ public class DeviceServerRack extends DeviceBase { * @param network owning network */ public DeviceServerRack(Vector3i position, Network network) { - super(position, EStorageDeviceType.SERVER_RACK, network); + super(position, EDeviceType.SERVER_RACK, network); } } diff --git a/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java b/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java index bfa9d5b..f24f62c 100644 --- a/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java +++ b/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java @@ -20,7 +20,7 @@ public class DeviceServerStorage extends DeviceBase { * @param network owning network */ public DeviceServerStorage(Vector3i position, Network network) { - super(position, EStorageDeviceType.SERVER_STORAGE, network); + super(position, EDeviceType.SERVER_STORAGE, network); items = new HashMap<>(); } diff --git a/src/main/java/com/github/hytech/storage/network/device/DeviceTerminal.java b/src/main/java/com/github/hytech/storage/network/device/DeviceTerminal.java index 5ef18f3..aefdf9c 100644 --- a/src/main/java/com/github/hytech/storage/network/device/DeviceTerminal.java +++ b/src/main/java/com/github/hytech/storage/network/device/DeviceTerminal.java @@ -14,6 +14,6 @@ public class DeviceTerminal extends DeviceBase { * @param network owning network */ public DeviceTerminal(Vector3i position, Network network) { - super(position, EStorageDeviceType.TERMINAL, network); + super(position, EDeviceType.TERMINAL, network); } } diff --git a/src/main/java/com/github/hytech/storage/network/device/EStorageDeviceType.java b/src/main/java/com/github/hytech/storage/network/device/EDeviceType.java similarity index 82% rename from src/main/java/com/github/hytech/storage/network/device/EStorageDeviceType.java rename to src/main/java/com/github/hytech/storage/network/device/EDeviceType.java index f4c1346..4e1718b 100644 --- a/src/main/java/com/github/hytech/storage/network/device/EStorageDeviceType.java +++ b/src/main/java/com/github/hytech/storage/network/device/EDeviceType.java @@ -3,7 +3,7 @@ /** * Enumerates the supported storage device types. */ -public enum EStorageDeviceType { +public enum EDeviceType { SERVER_RACK("Server_Rack"), SERVER_STORAGE("Server_Storage"), TERMINAL("Terminal"); @@ -15,7 +15,7 @@ public enum EStorageDeviceType { * * @param deviceType item id string */ - EStorageDeviceType(String deviceType) { + EDeviceType(String deviceType) { this.deviceType = deviceType; } @@ -34,8 +34,8 @@ public String getDeviceType() { * @param id item id string * @return device type or null if not matched */ - public static EStorageDeviceType fromId(String id) { - for (EStorageDeviceType type : values()) { + public static EDeviceType fromId(String id) { + for (EDeviceType type : values()) { if (type.deviceType.equalsIgnoreCase(id)) { return type; } diff --git a/src/main/java/com/github/hytech/storage/state/StateManager.java b/src/main/java/com/github/hytech/storage/state/StateManager.java index 5681b3c..e73636e 100644 --- a/src/main/java/com/github/hytech/storage/state/StateManager.java +++ b/src/main/java/com/github/hytech/storage/state/StateManager.java @@ -2,15 +2,10 @@ import com.github.hytech.storage.network.Network; import com.github.hytech.storage.network.SNetworkManager; -import com.github.hytech.storage.network.device.DeviceServerRack; import com.github.hytech.storage.network.device.DeviceServerStorage; -import com.github.hytech.storage.network.device.EStorageDeviceType; +import com.github.hytech.storage.network.device.EDeviceType; import com.github.hytech.storage.network.device.DeviceBase; -import com.github.hytech.storage.state.codec.DeviceCodec; -import com.github.hytech.storage.state.codec.NetworkCodec; -import com.github.hytech.storage.state.codec.NetworkManagerCodec; -import com.github.hytech.storage.state.codec.PositionCodec; -import com.github.hytech.storage.state.codec.StorageItemCodec; +import com.github.hytech.storage.state.codec.*; import com.github.hytech.storage.utility.Logger; import com.hypixel.hytale.codec.ExtraInfo; import com.hypixel.hytale.math.vector.Vector3i; @@ -103,7 +98,8 @@ public NetworkManagerCodec load() { */ private NetworkManagerCodec buildNetworkManagerCodec() { NetworkCodec[] networkCodecs = buildNetworkCodecs(); - return new NetworkManagerCodec(networkCodecs); + DeviceStorageCodec[] storageDeviceCodecs = buildDeviceStorageCodecs(); + return new NetworkManagerCodec(networkCodecs, storageDeviceCodecs); } /** @@ -118,26 +114,38 @@ private NetworkCodec[] buildNetworkCodecs() { NetworkCodec[] networkCodecs = new NetworkCodec[networks.size()]; for (int i = 0; i < networks.size(); i++) { - networkCodecs[i] = new NetworkCodec(buildDeviceCodecs(networks.get(i))); + networkCodecs[i] = new NetworkCodec(networks.get(i).getId(), buildDeviceBaseCodecs(networks.get(i))); } return networkCodecs; } + private DeviceStorageCodec[] buildDeviceStorageCodecs() { + List devicesStorage = new ArrayList<>(SNetworkManager.getInstance().getServerStorages().values()); + + DeviceStorageCodec[] deviceStorageCodecs = new DeviceStorageCodec[devicesStorage.size()]; + for (int i = 0; i < devicesStorage.size(); i++) { + DeviceServerStorage deviceStorage = devicesStorage.get(i); + EDeviceType deviceType = deviceStorage.getType(); + StorageItemCodec[] storageItems = buildStorageItemCodecs(deviceStorage); + deviceStorageCodecs[i] = new DeviceStorageCodec(deviceType.toString(), buildPositionCodec(deviceStorage), deviceStorage.getNetwork().getId(), storageItems); + } + return deviceStorageCodecs; + } + /** * Builds codec representations for devices in a network. * * @param network network to serialize * @return array of device codecs */ - private DeviceCodec[] buildDeviceCodecs(Network network) { - List devices = new ArrayList<>(network.getDevices().values()); - DeviceCodec[] deviceCodecs = new DeviceCodec[devices.size()]; - - for (int i = 0; i < devices.size(); i++) { - DeviceBase device = devices.get(i); - EStorageDeviceType deviceType = device.getType(); - StorageItemCodec[] storageItems = buildStorageItemCodecs(device); - deviceCodecs[i] = new DeviceCodec(deviceType.toString(), buildPositionCodec(device), storageItems); + private DeviceBaseCodec[] buildDeviceBaseCodecs(Network network) { + List devicesBase = new ArrayList<>(network.getBaseDevices().values()); + DeviceBaseCodec[] deviceCodecs = new DeviceBaseCodec[devicesBase.size()]; + + for (int i = 0; i < devicesBase.size(); i++) { + DeviceBase deviceBase = devicesBase.get(i); + EDeviceType deviceType = deviceBase.getType(); + deviceCodecs[i] = new DeviceBaseCodec(deviceType.toString(), buildPositionCodec(deviceBase)); } return deviceCodecs; diff --git a/src/main/java/com/github/hytech/storage/state/codec/DeviceBaseCodec.java b/src/main/java/com/github/hytech/storage/state/codec/DeviceBaseCodec.java new file mode 100644 index 0000000..626dec6 --- /dev/null +++ b/src/main/java/com/github/hytech/storage/state/codec/DeviceBaseCodec.java @@ -0,0 +1,47 @@ +package com.github.hytech.storage.state.codec; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +/** + * Serializable representation of a device for save data. + */ +public class DeviceBaseCodec { + public String type; + public PositionCodec position; + + /** + * Creates an empty device codec for decoding. + */ + public DeviceBaseCodec() { + this.type = ""; + this.position = new PositionCodec(0, 0, 0); + + } + + /** + * Creates a device codec with storage items. + * + * @param type device type string + * @param position device position + */ + public DeviceBaseCodec(String type, PositionCodec position) { + this.type = type; + this.position = position; + } + + public static final BuilderCodec CODEC = + BuilderCodec.builder(DeviceBaseCodec.class, DeviceBaseCodec::new) + .append( + new KeyedCodec<>("Type", Codec.STRING), + (b, v) -> b.type = v, + b -> b.type + ).add() + .append( + new KeyedCodec<>("Position", PositionCodec.CODEC), + (b, v) -> b.position = v, + b -> b.position + ).add() + .build(); +} diff --git a/src/main/java/com/github/hytech/storage/state/codec/DeviceCodec.java b/src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java similarity index 52% rename from src/main/java/com/github/hytech/storage/state/codec/DeviceCodec.java rename to src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java index 9b363a6..1664669 100644 --- a/src/main/java/com/github/hytech/storage/state/codec/DeviceCodec.java +++ b/src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java @@ -5,48 +5,31 @@ import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.codec.codecs.array.ArrayCodec; -/** - * Serializable representation of a device for save data. - */ -public class DeviceCodec { - public String type; - public PositionCodec position; +import java.util.UUID; + +public class DeviceStorageCodec extends DeviceBaseCodec { + public String networkId; public StorageItemCodec[] storageItems; - /** - * Creates an empty device codec for decoding. - */ - public DeviceCodec() { - this.type = ""; - this.position = new PositionCodec(0, 0, 0); - this.storageItems = new StorageItemCodec[0]; - } - /** - * Creates a device codec without storage items. - * - * @param type device type string - * @param position device position - */ - public DeviceCodec(String type, PositionCodec position) { - this.type = type; - this.position = position; + public DeviceStorageCodec() { + super(); + this.networkId = ""; this.storageItems = new StorageItemCodec[0]; } - /** - * Creates a device codec with storage items. - * - * @param type device type string - * @param position device position - * @param storageItems storage item list - */ - public DeviceCodec(String type, PositionCodec position, StorageItemCodec[] storageItems) { - this.type = type; - this.position = position; + + public DeviceStorageCodec(String type, PositionCodec position, UUID networkId, StorageItemCodec[] storageItems) { + super(type, position); + this.networkId = networkId.toString(); this.storageItems = storageItems; } - public static final BuilderCodec CODEC = - BuilderCodec.builder(DeviceCodec.class, DeviceCodec::new) + public static final BuilderCodec CODEC = + BuilderCodec.builder(DeviceStorageCodec.class, DeviceStorageCodec::new) + .append( + new KeyedCodec<>("NetworkId", Codec.STRING), + (b, v) -> b.networkId = v, + b -> b.networkId + ).add() .append( new KeyedCodec<>("Type", Codec.STRING), (b, v) -> b.type = v, diff --git a/src/main/java/com/github/hytech/storage/state/codec/NetworkCodec.java b/src/main/java/com/github/hytech/storage/state/codec/NetworkCodec.java index b975b61..564203d 100644 --- a/src/main/java/com/github/hytech/storage/state/codec/NetworkCodec.java +++ b/src/main/java/com/github/hytech/storage/state/codec/NetworkCodec.java @@ -1,38 +1,55 @@ package com.github.hytech.storage.state.codec; +import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.KeyedCodec; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.codec.codecs.array.ArrayCodec; +import java.util.UUID; + /** * Serializable representation of a network for save data. */ public class NetworkCodec { - public DeviceCodec[] deviceCodecs; + public String id; + public DeviceBaseCodec[] deviceCodecs; + + public NetworkCodec() { + this.id = ""; + this.deviceCodecs = new DeviceBaseCodec[0]; + } /** * Creates an empty network codec for decoding. */ - public NetworkCodec() { - this.deviceCodecs = new DeviceCodec[0]; + public NetworkCodec(UUID id) { + this.id = id.toString(); + this.deviceCodecs = new DeviceBaseCodec[0]; } + /** * Creates a network codec with devices. * * @param deviceCodecs device codecs */ - public NetworkCodec(DeviceCodec[] deviceCodecs) { + public NetworkCodec(UUID id, DeviceBaseCodec[] deviceCodecs) { + this.id = id.toString(); this.deviceCodecs = deviceCodecs; } public static final BuilderCodec CODEC = BuilderCodec.builder(NetworkCodec.class, NetworkCodec::new) + .append( + new KeyedCodec<>("Id", Codec.STRING), + (b, v) -> b.id = v, + b -> b.id + ).add() .append( new KeyedCodec<>( "Devices", - new ArrayCodec<>(DeviceCodec.CODEC, DeviceCodec[]::new) + new ArrayCodec<>(DeviceBaseCodec.CODEC, DeviceBaseCodec[]::new) ), - (NetworkCodec s, DeviceCodec[] v) -> s.deviceCodecs = v, + (NetworkCodec s, DeviceBaseCodec[] v) -> s.deviceCodecs = v, (NetworkCodec s) -> s.deviceCodecs ).add() .build(); diff --git a/src/main/java/com/github/hytech/storage/state/codec/NetworkManagerCodec.java b/src/main/java/com/github/hytech/storage/state/codec/NetworkManagerCodec.java index 974fa60..ae49507 100644 --- a/src/main/java/com/github/hytech/storage/state/codec/NetworkManagerCodec.java +++ b/src/main/java/com/github/hytech/storage/state/codec/NetworkManagerCodec.java @@ -9,20 +9,23 @@ */ public class NetworkManagerCodec { public NetworkCodec[] networkCodecs; + public DeviceStorageCodec[] storageDeviceCodecs; /** * Creates an empty network manager codec for decoding. */ public NetworkManagerCodec() { this.networkCodecs = new NetworkCodec[0]; + this.storageDeviceCodecs = new DeviceStorageCodec[0]; } /** * Creates a network manager codec with networks. * * @param networkCodecs network codecs */ - public NetworkManagerCodec(NetworkCodec[] networkCodecs) { + public NetworkManagerCodec(NetworkCodec[] networkCodecs, DeviceStorageCodec[] storageDeviceCodecs) { this.networkCodecs = networkCodecs; + this.storageDeviceCodecs = storageDeviceCodecs; } public static final BuilderCodec CODEC = @@ -35,5 +38,13 @@ public NetworkManagerCodec(NetworkCodec[] networkCodecs) { (NetworkManagerCodec s, NetworkCodec[] v) -> s.networkCodecs = v, (NetworkManagerCodec s) -> s.networkCodecs ).add() + .append( + new KeyedCodec<>( + "DevicesStorage", + new ArrayCodec<>(DeviceStorageCodec.CODEC, DeviceStorageCodec[]::new) + ), + (NetworkManagerCodec s, DeviceStorageCodec[] v) -> s.storageDeviceCodecs = v, + (NetworkManagerCodec s) -> s.storageDeviceCodecs + ).add() .build(); } diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java index 0406765..8eef8c6 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java @@ -1,7 +1,7 @@ package com.github.hytech.storage.world.events; import com.github.hytech.storage.network.SNetworkManager; -import com.github.hytech.storage.network.device.EStorageDeviceType; +import com.github.hytech.storage.network.device.EDeviceType; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Store; @@ -43,8 +43,13 @@ public void handle( ) { try { Vector3i position = breakBlockEvent.getTargetBlock(); - String itemId = breakBlockEvent.getItemInHand().getItemId(); - EStorageDeviceType deviceType = EStorageDeviceType.fromId(itemId); + String brokenBlockId = breakBlockEvent.getBlockType() == null + ? null + : breakBlockEvent.getBlockType().getId(); + EDeviceType deviceType = EDeviceType.fromId(brokenBlockId); + if (deviceType == null) { + return; + } SNetworkManager.getInstance().onDeviceDestroyed(position); } catch (Exception e) { diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java index 2d08a60..0715282 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java @@ -1,8 +1,7 @@ package com.github.hytech.storage.world.events; import com.github.hytech.storage.network.SNetworkManager; -import com.github.hytech.storage.network.device.EStorageDeviceType; -import com.github.hytech.storage.utility.Logger; +import com.github.hytech.storage.network.device.EDeviceType; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Store; @@ -45,7 +44,7 @@ public void handle( try { Vector3i position = placeBlockEvent.getTargetBlock(); String itemId = placeBlockEvent.getItemInHand().getItemId(); - EStorageDeviceType deviceType = EStorageDeviceType.fromId(itemId); + EDeviceType deviceType = EDeviceType.fromId(itemId); SNetworkManager.getInstance().onDevicePlaced(deviceType, position); } catch (Exception e) { From b82eb42a3eac97d8cfbf689ce86e1eee877cd5b6 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 14:43:57 -0500 Subject: [PATCH 06/12] Separate storage devices from other devices in save file and codecs. Rename some classes to be more consistent. New storage device codec inherits base device codec. --- HT_PBI_OVERVIEW.md | 14 ++ .../github/hytech/storage/HytechStorage.java | 4 + .../storage/network/SNetworkManager.java | 185 +++++++++++++++++- .../network/device/DeviceServerStorage.java | 10 + .../hytech/storage/state/StateManager.java | 16 +- .../state/codec/DeviceStorageCodec.java | 28 +++ .../world/events/BlockBreakEventSystem.java | 71 ++++++- .../events/BlockPlacementEventSystem.java | 8 +- 8 files changed, 329 insertions(+), 7 deletions(-) diff --git a/HT_PBI_OVERVIEW.md b/HT_PBI_OVERVIEW.md index 291da4f..6294233 100644 --- a/HT_PBI_OVERVIEW.md +++ b/HT_PBI_OVERVIEW.md @@ -200,6 +200,20 @@ What they are: How we use them: - Track network topology when devices are placed or destroyed. +### DropItemEvent.Drop +What it is: +- ECS event fired when an item stack is about to be dropped into the world. + +How we use it: +- Attach `HytechStorageId` metadata to dropped loaded storage blocks so they stay non-stackable and can rebind saved contents on placement. + +### InteractivelyPickupItemEvent +What it is: +- ECS event fired while an item stack is being picked up. + +How we use it: +- Fallback metadata tagging for loaded storage block drops to prevent stack-merging with plain storage blocks. + ### CommandContext / CommandBase What they are: - Base types for server commands. diff --git a/src/main/java/com/github/hytech/storage/HytechStorage.java b/src/main/java/com/github/hytech/storage/HytechStorage.java index 1678fea..cb03ec2 100644 --- a/src/main/java/com/github/hytech/storage/HytechStorage.java +++ b/src/main/java/com/github/hytech/storage/HytechStorage.java @@ -6,6 +6,8 @@ import com.github.hytech.storage.world.events.BlockBreakEventSystem; import com.github.hytech.storage.world.events.BlockPlacementEventSystem; import com.github.hytech.storage.world.events.BlockUseEventSystem; +import com.github.hytech.storage.world.events.DropItemEventSystem; +import com.github.hytech.storage.world.events.ItemPickupEventSystem; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; @@ -58,6 +60,8 @@ private void registerCommands() { private void registerEventSystems() { this.getEntityStoreRegistry().registerSystem(new BlockPlacementEventSystem()); this.getEntityStoreRegistry().registerSystem(new BlockBreakEventSystem()); + this.getEntityStoreRegistry().registerSystem(new DropItemEventSystem()); + this.getEntityStoreRegistry().registerSystem(new ItemPickupEventSystem()); this.getEntityStoreRegistry().registerSystem(new BlockUseEventSystem()); } } diff --git a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java index e1291ef..cb1a1ab 100644 --- a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java +++ b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java @@ -13,13 +13,16 @@ * Singleton that manages networks and their devices. */ public class SNetworkManager { + public static final String STORAGE_ID_METADATA_KEY = "HytechStorageId"; private static SNetworkManager instance; - private boolean restoringFromSaveFile = false; + private boolean restoringFromSaveFile = false; private final Map networks = new HashMap<>(); private final Map devices = new HashMap<>(); private final Map serverRacks = new HashMap<>(); private final Map serverStorages = new HashMap<>(); private final Map terminals = new HashMap<>(); + private final Map detachedStorageDevices = new HashMap<>(); + private final Deque pendingDroppedStorageIds = new ArrayDeque<>(); /** * Creates the singleton manager. @@ -81,6 +84,14 @@ public Map getTerminals() { return terminals; } + public Map getDetachedStorageDevices() { + return detachedStorageDevices; + } + + public String consumePendingDroppedStorageId() { + return pendingDroppedStorageIds.pollFirst(); + } + /** * Adds a device to the world and merges or creates networks as needed. * @@ -88,6 +99,10 @@ public Map getTerminals() { * @param position block position */ public void onDevicePlaced(EDeviceType deviceType, Vector3i position) { + onDevicePlaced(deviceType, position, null); + } + + public void onDevicePlaced(EDeviceType deviceType, Vector3i position, String storageIdFromItem) { List neighbors = DetectNeighbors.detectNeighbor(position, devices); Network network; @@ -117,6 +132,26 @@ public void onDevicePlaced(EDeviceType deviceType, Vector3i position) { break; case SERVER_STORAGE: DeviceServerStorage storage = new DeviceServerStorage(position, network); + DeviceStorageCodec detachedStorageCodec = null; + if (storageIdFromItem != null && !storageIdFromItem.isBlank()) { + detachedStorageCodec = detachedStorageDevices.remove(storageIdFromItem); + storage.setStorageId(storageIdFromItem); + } + if (detachedStorageCodec == null) { + detachedStorageCodec = consumeDetachedStorageByPosition(position); + if (detachedStorageCodec != null && detachedStorageCodec.storageId != null && !detachedStorageCodec.storageId.isBlank()) { + storage.setStorageId(detachedStorageCodec.storageId); + } + } + if (detachedStorageCodec != null) { + applyStorageItems(storage, detachedStorageCodec.storageItems); + if (storage.getStorageId().isBlank()) { + storage.setStorageId(detachedStorageCodec.storageId); + } + } + if (storage.getStorageId().isBlank()) { + storage.setStorageId(UUID.randomUUID().toString()); + } network.addDevice(storage); devices.put(position, storage); serverStorages.put(position, storage); @@ -156,6 +191,16 @@ private void joinNetworks(Network networkA, Network networkB) { * @param position block position */ public void onDeviceDestroyed(Vector3i position) { + onDeviceDestroyed(position, true); + } + + /** + * Removes a device and updates network membership. + * + * @param position block position + * @param preserveStorageData when true, storage block contents remain in detached save state + */ + public void onDeviceDestroyed(Vector3i position, boolean preserveStorageData) { DeviceBase destroyed = devices.get(position); if (destroyed == null) { Logger.getInstance().log("destroyed device was null at position: " + position); @@ -171,7 +216,37 @@ public void onDeviceDestroyed(Vector3i position) { switch (destroyed.getType()) { case TERMINAL -> terminals.remove(position); - case SERVER_STORAGE -> serverStorages.remove(position); + case SERVER_STORAGE -> { + if (preserveStorageData + && destroyed instanceof DeviceServerStorage storageDevice + && storageHasItems(storageDevice)) { + String storageId = storageDevice.getStorageId(); + if (storageId == null || storageId.isBlank()) { + storageId = UUID.randomUUID().toString(); + storageDevice.setStorageId(storageId); + } + StorageItemCodec[] storageItems = buildStorageItemCodecs(storageDevice); + detachedStorageDevices.put( + storageId, + new DeviceStorageCodec( + storageDevice.getType().toString(), + new PositionCodec(position.x, position.y, position.z), + "", + storageId, + storageItems + ) + ); + pendingDroppedStorageIds.addLast(storageId); + } else { + if (destroyed instanceof DeviceServerStorage storageDevice) { + String storageId = storageDevice.getStorageId(); + if (storageId != null && !storageId.isBlank()) { + detachedStorageDevices.remove(storageId); + } + } + } + serverStorages.remove(position); + } case SERVER_RACK -> serverRacks.remove(position); } @@ -222,11 +297,18 @@ public void restoreIntoNetworkManager(NetworkManagerCodec networkManagerCodec) { devices.clear(); serverRacks.clear(); serverStorages.clear(); + detachedStorageDevices.clear(); + pendingDroppedStorageIds.clear(); networks.clear(); for (NetworkCodec networkCodec : networkManagerCodec.networkCodecs) { restoreIntoNetwork(networkCodec); } + if (networkManagerCodec.storageDeviceCodecs != null) { + for (DeviceStorageCodec storageCodec : networkManagerCodec.storageDeviceCodecs) { + restoreStorageCodec(storageCodec); + } + } restoringFromSaveFile = false; @@ -245,7 +327,12 @@ public void restoreIntoNetwork(NetworkCodec networkCodec) { return; } - UUID networkId = UUID.randomUUID(); + UUID networkId; + try { + networkId = UUID.fromString(networkCodec.id); + } catch (Exception ignored) { + networkId = UUID.randomUUID(); + } Network network = new Network(networkId); for (DeviceBaseCodec deviceCodec : deviceCodecs) { @@ -282,6 +369,9 @@ public void restoreIntoDevice(Network network, DeviceBaseCodec deviceCodec) { devices.put(position, terminal); network.addDevice(terminal); break; + case SERVER_STORAGE: + // Storage devices are restored from NetworkManagerCodec.storageDeviceCodecs. + break; case SERVER_RACK: case null: default: @@ -318,4 +408,93 @@ private void applyStorageItems(DeviceServerStorage serverStorage, StorageItemCod } serverStorage.setItems(items); } + + private StorageItemCodec[] buildStorageItemCodecs(DeviceServerStorage storageDevice) { + List storageItems = new ArrayList<>(); + for (Map.Entry entry : storageDevice.getItems().entrySet()) { + storageItems.add(new StorageItemCodec(entry.getKey(), entry.getValue())); + } + return storageItems.toArray(new StorageItemCodec[0]); + } + + private void restoreStorageCodec(DeviceStorageCodec storageCodec) { + if (storageCodec == null || storageCodec.position == null) { + return; + } + Vector3i position = restoreIntoPosition(storageCodec.position); + if (storageCodec.networkId == null || storageCodec.networkId.isBlank()) { + String storageId = storageCodec.storageId; + if (storageId == null || storageId.isBlank()) { + storageId = UUID.randomUUID().toString(); + storageCodec.storageId = storageId; + } + detachedStorageDevices.put(storageId, storageCodec); + return; + } + + UUID networkId; + try { + networkId = UUID.fromString(storageCodec.networkId); + } catch (Exception ignored) { + String storageId = storageCodec.storageId; + if (storageId == null || storageId.isBlank()) { + storageId = UUID.randomUUID().toString(); + storageCodec.storageId = storageId; + } + detachedStorageDevices.put(storageId, storageCodec); + return; + } + + Network network = networks.get(networkId); + if (network == null) { + String storageId = storageCodec.storageId; + if (storageId == null || storageId.isBlank()) { + storageId = UUID.randomUUID().toString(); + storageCodec.storageId = storageId; + } + detachedStorageDevices.put(storageId, storageCodec); + return; + } + + DeviceServerStorage serverStorage = new DeviceServerStorage(position, network); + if (storageCodec.storageId == null || storageCodec.storageId.isBlank()) { + storageCodec.storageId = UUID.randomUUID().toString(); + } + serverStorage.setStorageId(storageCodec.storageId); + applyStorageItems(serverStorage, storageCodec.storageItems); + serverStorages.put(position, serverStorage); + devices.put(position, serverStorage); + network.addDevice(serverStorage); + } + + private boolean storageHasItems(DeviceServerStorage storageDevice) { + if (storageDevice == null || storageDevice.getItems() == null || storageDevice.getItems().isEmpty()) { + return false; + } + for (Map.Entry entry : storageDevice.getItems().entrySet()) { + if (entry.getValue() != null && entry.getValue() > 0) { + return true; + } + } + return false; + } + + private DeviceStorageCodec consumeDetachedStorageByPosition(Vector3i position) { + if (position == null) { + return null; + } + Iterator> iterator = detachedStorageDevices.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + DeviceStorageCodec codec = entry.getValue(); + if (codec == null || codec.position == null) { + continue; + } + if (codec.position.x == position.x && codec.position.y == position.y && codec.position.z == position.z) { + iterator.remove(); + return codec; + } + } + return null; + } } diff --git a/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java b/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java index f24f62c..9d52dc0 100644 --- a/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java +++ b/src/main/java/com/github/hytech/storage/network/device/DeviceServerStorage.java @@ -11,6 +11,7 @@ */ public class DeviceServerStorage extends DeviceBase { private static final int STORAGE_MAX = 1024; + private String storageId; private final Map items; /** @@ -21,9 +22,18 @@ public class DeviceServerStorage extends DeviceBase { */ public DeviceServerStorage(Vector3i position, Network network) { super(position, EDeviceType.SERVER_STORAGE, network); + this.storageId = ""; items = new HashMap<>(); } + public String getStorageId() { + return storageId; + } + + public void setStorageId(String storageId) { + this.storageId = storageId == null ? "" : storageId; + } + /** * Returns the stored item counts. * diff --git a/src/main/java/com/github/hytech/storage/state/StateManager.java b/src/main/java/com/github/hytech/storage/state/StateManager.java index e73636e..c0debc3 100644 --- a/src/main/java/com/github/hytech/storage/state/StateManager.java +++ b/src/main/java/com/github/hytech/storage/state/StateManager.java @@ -121,13 +121,25 @@ private NetworkCodec[] buildNetworkCodecs() { private DeviceStorageCodec[] buildDeviceStorageCodecs() { List devicesStorage = new ArrayList<>(SNetworkManager.getInstance().getServerStorages().values()); + List detachedStorageCodecs = new ArrayList<>(SNetworkManager.getInstance().getDetachedStorageDevices().values()); - DeviceStorageCodec[] deviceStorageCodecs = new DeviceStorageCodec[devicesStorage.size()]; + DeviceStorageCodec[] deviceStorageCodecs = new DeviceStorageCodec[devicesStorage.size() + detachedStorageCodecs.size()]; for (int i = 0; i < devicesStorage.size(); i++) { DeviceServerStorage deviceStorage = devicesStorage.get(i); EDeviceType deviceType = deviceStorage.getType(); StorageItemCodec[] storageItems = buildStorageItemCodecs(deviceStorage); - deviceStorageCodecs[i] = new DeviceStorageCodec(deviceType.toString(), buildPositionCodec(deviceStorage), deviceStorage.getNetwork().getId(), storageItems); + String storageId = deviceStorage.getStorageId(); + deviceStorageCodecs[i] = new DeviceStorageCodec( + deviceType.toString(), + buildPositionCodec(deviceStorage), + deviceStorage.getNetwork().getId().toString(), + storageId, + storageItems + ); + } + + for (int i = 0; i < detachedStorageCodecs.size(); i++) { + deviceStorageCodecs[devicesStorage.size() + i] = detachedStorageCodecs.get(i); } return deviceStorageCodecs; } diff --git a/src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java b/src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java index 1664669..7df0d54 100644 --- a/src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java +++ b/src/main/java/com/github/hytech/storage/state/codec/DeviceStorageCodec.java @@ -9,17 +9,40 @@ public class DeviceStorageCodec extends DeviceBaseCodec { public String networkId; + public String storageId; public StorageItemCodec[] storageItems; public DeviceStorageCodec() { super(); this.networkId = ""; + this.storageId = ""; this.storageItems = new StorageItemCodec[0]; } public DeviceStorageCodec(String type, PositionCodec position, UUID networkId, StorageItemCodec[] storageItems) { super(type, position); this.networkId = networkId.toString(); + this.storageId = ""; + this.storageItems = storageItems; + } + + public DeviceStorageCodec(String type, PositionCodec position, String networkId, StorageItemCodec[] storageItems) { + super(type, position); + this.networkId = networkId == null ? "" : networkId; + this.storageId = ""; + this.storageItems = storageItems; + } + + public DeviceStorageCodec( + String type, + PositionCodec position, + String networkId, + String storageId, + StorageItemCodec[] storageItems + ) { + super(type, position); + this.networkId = networkId == null ? "" : networkId; + this.storageId = storageId == null ? "" : storageId; this.storageItems = storageItems; } @@ -30,6 +53,11 @@ public DeviceStorageCodec(String type, PositionCodec position, UUID networkId, S (b, v) -> b.networkId = v, b -> b.networkId ).add() + .append( + new KeyedCodec<>("StorageId", Codec.STRING), + (b, v) -> b.storageId = v, + b -> b.storageId + ).add() .append( new KeyedCodec<>("Type", Codec.STRING), (b, v) -> b.type = v, diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java index 8eef8c6..b581620 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java @@ -4,12 +4,19 @@ import com.github.hytech.storage.network.device.EDeviceType; 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.EntityEventSystem; import com.hypixel.hytale.math.vector.Vector3i; +import com.hypixel.hytale.protocol.GameMode; +import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.codec.Codec; import org.checkerframework.checker.nullness.compatqual.NonNullDecl; import org.checkerframework.checker.nullness.compatqual.NullableDecl; @@ -50,12 +57,74 @@ public void handle( if (deviceType == null) { return; } - SNetworkManager.getInstance().onDeviceDestroyed(position); + + Ref playerRef = archetypeChunk.getReferenceTo(var1); + Player player = store.getComponent(playerRef, Player.getComponentType()); + boolean preserveStorageData = player == null || player.getGameMode() != GameMode.Creative; + + SNetworkManager.getInstance().onDeviceDestroyed(position, preserveStorageData); + if (deviceType == EDeviceType.SERVER_STORAGE && preserveStorageData && player != null) { + tryAssignPendingStorageIdToInventory(playerRef, store, player); + } } catch (Exception e) { } } + private void tryAssignPendingStorageIdToInventory( + Ref playerRef, + Store store, + Player player + ) { + ItemContainer inventory = player.getInventory().getCombinedEverything(); + short slotIndex = findFirstPlainStorageSlot(inventory); + if (slotIndex < 0) { + return; + } + + String pendingStorageId = SNetworkManager.getInstance().consumePendingDroppedStorageId(); + if (pendingStorageId == null || pendingStorageId.isBlank()) { + return; + } + + ItemStack stack = inventory.getItemStack(slotIndex); + if (stack == null || stack.isEmpty()) { + return; + } + + if (stack.getQuantity() <= 1) { + inventory.setItemStackForSlot( + slotIndex, + stack.withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId) + ); + return; + } + + inventory.setItemStackForSlot(slotIndex, stack.withQuantity(stack.getQuantity() - 1)); + ItemStack taggedSingle = new ItemStack(stack.getItemId(), 1) + .withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId); + SimpleItemContainer.addOrDropItemStack(store, playerRef, inventory, taggedSingle); + } + + private short findFirstPlainStorageSlot(ItemContainer inventory) { + short capacity = inventory.getCapacity(); + for (short i = 0; i < capacity; i++) { + ItemStack stack = inventory.getItemStack(i); + if (stack == null || stack.isEmpty()) { + continue; + } + if (EDeviceType.fromId(stack.getItemId()) != EDeviceType.SERVER_STORAGE) { + continue; + } + String storageId = stack.getFromMetadataOrNull(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING); + if (storageId != null && !storageId.isBlank()) { + continue; + } + return i; + } + return -1; + } + /** * Declares the ECS query for this system. * diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java index 0715282..f5b8941 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockPlacementEventSystem.java @@ -2,6 +2,7 @@ import com.github.hytech.storage.network.SNetworkManager; import com.github.hytech.storage.network.device.EDeviceType; +import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Store; @@ -45,7 +46,12 @@ public void handle( Vector3i position = placeBlockEvent.getTargetBlock(); String itemId = placeBlockEvent.getItemInHand().getItemId(); EDeviceType deviceType = EDeviceType.fromId(itemId); - SNetworkManager.getInstance().onDevicePlaced(deviceType, position); + String storageId = null; + if (deviceType == EDeviceType.SERVER_STORAGE) { + storageId = placeBlockEvent.getItemInHand() + .getFromMetadataOrNull(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING); + } + SNetworkManager.getInstance().onDevicePlaced(deviceType, position, storageId); } catch (Exception e) { } From e67fd9177f88fac57455ab360d3dda11b37310ca Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 15:20:35 -0500 Subject: [PATCH 07/12] Separate storage devices from other devices in save file and codecs. Rename some classes to be more consistent. New storage device codec inherits base device codec. --- HT_PBI_OVERVIEW.md | 7 ++ .../github/hytech/storage/HytechStorage.java | 3 + .../storage/network/SNetworkManager.java | 29 ++++++ .../storage/ui/TerminalInventoryPage.java | 26 ++++- .../utility/commands/StorageInfoCommand.java | 52 ++++++++++ .../world/events/BlockBreakEventSystem.java | 19 ++++ .../world/events/DropItemEventSystem.java | 71 ++++++++++++++ .../world/events/ItemPickupEventSystem.java | 94 +++++++++++++++++++ .../events/SwitchActiveSlotEventSystem.java | 93 ++++++++++++++++++ 9 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/github/hytech/storage/utility/commands/StorageInfoCommand.java create mode 100644 src/main/java/com/github/hytech/storage/world/events/DropItemEventSystem.java create mode 100644 src/main/java/com/github/hytech/storage/world/events/ItemPickupEventSystem.java create mode 100644 src/main/java/com/github/hytech/storage/world/events/SwitchActiveSlotEventSystem.java diff --git a/HT_PBI_OVERVIEW.md b/HT_PBI_OVERVIEW.md index 6294233..5d9dd69 100644 --- a/HT_PBI_OVERVIEW.md +++ b/HT_PBI_OVERVIEW.md @@ -214,6 +214,13 @@ What it is: How we use it: - Fallback metadata tagging for loaded storage block drops to prevent stack-merging with plain storage blocks. +### SwitchActiveSlotEvent +What it is: +- ECS event fired when the player changes active hotbar/tool slot. + +How we use it: +- When a tagged loaded storage block is selected, we show an on-screen notification with stored item count. + ### CommandContext / CommandBase What they are: - Base types for server commands. diff --git a/src/main/java/com/github/hytech/storage/HytechStorage.java b/src/main/java/com/github/hytech/storage/HytechStorage.java index cb03ec2..aae388d 100644 --- a/src/main/java/com/github/hytech/storage/HytechStorage.java +++ b/src/main/java/com/github/hytech/storage/HytechStorage.java @@ -8,6 +8,7 @@ import com.github.hytech.storage.world.events.BlockUseEventSystem; import com.github.hytech.storage.world.events.DropItemEventSystem; import com.github.hytech.storage.world.events.ItemPickupEventSystem; +import com.github.hytech.storage.world.events.SwitchActiveSlotEventSystem; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; @@ -52,6 +53,7 @@ private void registerCommands() { this.getCommandRegistry().registerCommand(new ListNetworksCommand()); this.getCommandRegistry().registerCommand(new ListServerRacksCommand()); this.getCommandRegistry().registerCommand(new ListServerStoragesCommand()); + this.getCommandRegistry().registerCommand(new StorageInfoCommand()); } /** @@ -62,6 +64,7 @@ private void registerEventSystems() { this.getEntityStoreRegistry().registerSystem(new BlockBreakEventSystem()); this.getEntityStoreRegistry().registerSystem(new DropItemEventSystem()); this.getEntityStoreRegistry().registerSystem(new ItemPickupEventSystem()); + this.getEntityStoreRegistry().registerSystem(new SwitchActiveSlotEventSystem()); this.getEntityStoreRegistry().registerSystem(new BlockUseEventSystem()); } } diff --git a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java index cb1a1ab..29535ec 100644 --- a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java +++ b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java @@ -92,6 +92,35 @@ public String consumePendingDroppedStorageId() { return pendingDroppedStorageIds.pollFirst(); } + public int getStoredCountByStorageId(String storageId) { + if (storageId == null || storageId.isBlank()) { + return 0; + } + + int total = 0; + DeviceStorageCodec detached = detachedStorageDevices.get(storageId); + if (detached != null && detached.storageItems != null) { + for (StorageItemCodec item : detached.storageItems) { + if (item != null) { + total += Math.max(0, item.count); + } + } + return total; + } + + for (DeviceServerStorage storage : serverStorages.values()) { + if (!storageId.equals(storage.getStorageId())) { + continue; + } + for (Integer count : storage.getItems().values()) { + total += Math.max(0, count == null ? 0 : count); + } + return total; + } + + return 0; + } + /** * Adds a device to the world and merges or creates networks as needed. * diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index ada38db..bb39aa4 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -2,6 +2,7 @@ import com.github.hytech.storage.network.Network; import com.github.hytech.storage.network.SNetworkManager; +import com.github.hytech.storage.network.device.EDeviceType; import com.github.hytech.storage.network.device.DeviceServerStorage; import com.github.hytech.storage.network.device.DeviceTerminal; import com.github.hytech.storage.state.StateManager; @@ -943,18 +944,39 @@ private void updatePlayerInventoryGrids( List storageData = new ArrayList<>(); for (int i = 0; i < PLAYER_STORAGE_VISIBLE_SLOTS; i++) { ItemStack stack = i < storage.getCapacity() ? storage.getItemStack((short) i) : null; - storageData.add((stack == null || stack.isEmpty()) ? new ItemGridSlot() : new ItemGridSlot(stack)); + storageData.add((stack == null || stack.isEmpty()) + ? new ItemGridSlot() + : new ItemGridSlot(toPlayerGridDisplayStack(stack))); } commands.set("#PlayerStorageGrid.Slots", storageData); List hotbarData = new ArrayList<>(); for (int i = 0; i < PLAYER_HOTBAR_VISIBLE_SLOTS; i++) { ItemStack stack = i < hotbar.getCapacity() ? hotbar.getItemStack((short) i) : null; - hotbarData.add((stack == null || stack.isEmpty()) ? new ItemGridSlot() : new ItemGridSlot(stack)); + hotbarData.add((stack == null || stack.isEmpty()) + ? new ItemGridSlot() + : new ItemGridSlot(toPlayerGridDisplayStack(stack))); } commands.set("#PlayerHotbarGrid.Slots", hotbarData); } + /** + * Returns a UI-safe display stack for player grids. + * Mixing plain and metadata-tagged storage blocks can fail CustomUI slot serialization, + * so we strip metadata for storage block display only. + */ + private ItemStack toPlayerGridDisplayStack(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return stack; + } + if (SNetworkManager.STORAGE_ID_METADATA_KEY != null + && stack.getFromMetadataOrNull(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING) != null + && EDeviceType.fromId(stack.getItemId()) == EDeviceType.SERVER_STORAGE) { + return new ItemStack(stack.getItemId(), stack.getQuantity()); + } + return stack; + } + /** * Updates the side storage usage meter and labels. */ diff --git a/src/main/java/com/github/hytech/storage/utility/commands/StorageInfoCommand.java b/src/main/java/com/github/hytech/storage/utility/commands/StorageInfoCommand.java new file mode 100644 index 0000000..87aa178 --- /dev/null +++ b/src/main/java/com/github/hytech/storage/utility/commands/StorageInfoCommand.java @@ -0,0 +1,52 @@ +package com.github.hytech.storage.utility.commands; + +import com.github.hytech.storage.network.SNetworkManager; +import com.github.hytech.storage.network.device.EDeviceType; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.ItemStack; + +import javax.annotation.Nonnull; +import java.util.Locale; + +/** + * Shows info about the storage block currently held in the active slot. + */ +public class StorageInfoCommand extends CommandBase { + public StorageInfoCommand() { + super("storageinfo", "Shows details for the held storage block."); + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + if (!ctx.isPlayer()) { + ctx.sendMessage(Message.raw("This command can only be run by a player.")); + return; + } + + Player player = ctx.senderAs(Player.class); + ItemStack inHand = player.getInventory().getItemInHand(); + if (inHand == null || inHand.isEmpty()) { + ctx.sendMessage(Message.raw("Hold a storage block first.")); + return; + } + if (EDeviceType.fromId(inHand.getItemId()) != EDeviceType.SERVER_STORAGE) { + ctx.sendMessage(Message.raw("Held item is not a storage block.")); + return; + } + + String storageId = inHand.getFromMetadataOrNull(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING); + if (storageId == null || storageId.isBlank()) { + ctx.sendMessage(Message.raw("Held storage block is empty/unloaded (no portable storage id).")); + return; + } + + int storedCount = SNetworkManager.getInstance().getStoredCountByStorageId(storageId); + ctx.sendMessage(Message.raw( + "Stored items: " + String.format(Locale.US, "%,d", Math.max(0, storedCount)) + )); + } +} diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java index b581620..2af9f21 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java @@ -10,6 +10,7 @@ import com.hypixel.hytale.component.system.EntityEventSystem; import com.hypixel.hytale.math.vector.Vector3i; import com.hypixel.hytale.protocol.GameMode; +import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; import com.hypixel.hytale.server.core.inventory.ItemStack; @@ -20,6 +21,8 @@ import org.checkerframework.checker.nullness.compatqual.NonNullDecl; import org.checkerframework.checker.nullness.compatqual.NullableDecl; +import java.util.Locale; + /** * ECS system that reacts to block break events. */ @@ -97,6 +100,7 @@ private void tryAssignPendingStorageIdToInventory( slotIndex, stack.withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId) ); + sendLoadedStorageMessage(player, pendingStorageId); return; } @@ -104,6 +108,21 @@ private void tryAssignPendingStorageIdToInventory( ItemStack taggedSingle = new ItemStack(stack.getItemId(), 1) .withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId); SimpleItemContainer.addOrDropItemStack(store, playerRef, inventory, taggedSingle); + sendLoadedStorageMessage(player, pendingStorageId); + } + + private void sendLoadedStorageMessage(Player player, String storageId) { + if (player == null || storageId == null || storageId.isBlank()) { + return; + } + int storedCount = SNetworkManager.getInstance().getStoredCountByStorageId(storageId); + player.sendMessage(Message.raw( + "[Hytech Storage] Loaded storage block created (" + formatNumber(storedCount) + " item(s))." + )); + } + + private String formatNumber(int value) { + return String.format(Locale.US, "%,d", Math.max(0, value)); } private short findFirstPlainStorageSlot(ItemContainer inventory) { diff --git a/src/main/java/com/github/hytech/storage/world/events/DropItemEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/DropItemEventSystem.java new file mode 100644 index 0000000..7322210 --- /dev/null +++ b/src/main/java/com/github/hytech/storage/world/events/DropItemEventSystem.java @@ -0,0 +1,71 @@ +package com.github.hytech.storage.world.events; + +import com.github.hytech.storage.network.SNetworkManager; +import com.github.hytech.storage.network.device.EDeviceType; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.EntityEventSystem; +import com.hypixel.hytale.server.core.event.events.ecs.DropItemEvent; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +/** + * Assigns portable storage identity metadata to dropped loaded storage blocks. + */ +public class DropItemEventSystem extends EntityEventSystem { + public DropItemEventSystem() { + super(DropItemEvent.Drop.class); + } + + @Override + public void handle( + int var1, + @NonNullDecl ArchetypeChunk archetypeChunk, + @NonNullDecl Store store, + @NonNullDecl CommandBuffer commandBuffer, + @NonNullDecl DropItemEvent.Drop dropItemEvent + ) { + try { + ItemStack itemStack = dropItemEvent.getItemStack(); + if (itemStack == null || itemStack.isEmpty()) { + return; + } + if (EDeviceType.fromId(itemStack.getItemId()) != EDeviceType.SERVER_STORAGE) { + return; + } + if (itemStack.getQuantity() != 1) { + return; + } + + String existingStorageId = itemStack.getFromMetadataOrNull( + SNetworkManager.STORAGE_ID_METADATA_KEY, + Codec.STRING + ); + if (existingStorageId != null && !existingStorageId.isBlank()) { + return; + } + + String pendingStorageId = SNetworkManager.getInstance().consumePendingDroppedStorageId(); + if (pendingStorageId == null || pendingStorageId.isBlank()) { + return; + } + + dropItemEvent.setItemStack( + itemStack.withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId) + ); + } catch (Exception ignored) { + } + } + + @NullableDecl + @Override + public Query getQuery() { + return Query.any(); + } +} + diff --git a/src/main/java/com/github/hytech/storage/world/events/ItemPickupEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/ItemPickupEventSystem.java new file mode 100644 index 0000000..bd347c2 --- /dev/null +++ b/src/main/java/com/github/hytech/storage/world/events/ItemPickupEventSystem.java @@ -0,0 +1,94 @@ +package com.github.hytech.storage.world.events; + +import com.github.hytech.storage.network.SNetworkManager; +import com.github.hytech.storage.network.device.EDeviceType; +import com.hypixel.hytale.codec.Codec; +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.EntityEventSystem; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.ecs.InteractivelyPickupItemEvent; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +import java.util.Locale; + +/** + * Fallback metadata tagging on pickup for storage block drops. + * This prevents loaded storage blocks from merging into plain storage stacks. + */ +public class ItemPickupEventSystem extends EntityEventSystem { + public ItemPickupEventSystem() { + super(InteractivelyPickupItemEvent.class); + } + + @Override + public void handle( + int var1, + @NonNullDecl ArchetypeChunk archetypeChunk, + @NonNullDecl Store store, + @NonNullDecl CommandBuffer commandBuffer, + @NonNullDecl InteractivelyPickupItemEvent pickupEvent + ) { + try { + Ref playerRef = archetypeChunk.getReferenceTo(var1); + Player player = store.getComponent(playerRef, Player.getComponentType()); + ItemStack itemStack = pickupEvent.getItemStack(); + if (itemStack == null || itemStack.isEmpty()) { + return; + } + if (EDeviceType.fromId(itemStack.getItemId()) != EDeviceType.SERVER_STORAGE) { + return; + } + if (itemStack.getQuantity() != 1) { + return; + } + + String existingStorageId = itemStack.getFromMetadataOrNull( + SNetworkManager.STORAGE_ID_METADATA_KEY, + Codec.STRING + ); + if (existingStorageId != null && !existingStorageId.isBlank()) { + sendLoadedStorageMessage(player, existingStorageId); + return; + } + + String pendingStorageId = SNetworkManager.getInstance().consumePendingDroppedStorageId(); + if (pendingStorageId == null || pendingStorageId.isBlank()) { + return; + } + + pickupEvent.setItemStack( + itemStack.withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId) + ); + sendLoadedStorageMessage(player, pendingStorageId); + } catch (Exception ignored) { + } + } + + private void sendLoadedStorageMessage(Player player, String storageId) { + if (player == null || storageId == null || storageId.isBlank()) { + return; + } + int storedCount = SNetworkManager.getInstance().getStoredCountByStorageId(storageId); + player.sendMessage(Message.raw( + "[Hytech Storage] Loaded storage block picked up (" + formatNumber(storedCount) + " item(s))." + )); + } + + private String formatNumber(int value) { + return String.format(Locale.US, "%,d", Math.max(0, value)); + } + + @NullableDecl + @Override + public Query getQuery() { + return Query.any(); + } +} diff --git a/src/main/java/com/github/hytech/storage/world/events/SwitchActiveSlotEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/SwitchActiveSlotEventSystem.java new file mode 100644 index 0000000..1d9357e --- /dev/null +++ b/src/main/java/com/github/hytech/storage/world/events/SwitchActiveSlotEventSystem.java @@ -0,0 +1,93 @@ +package com.github.hytech.storage.world.events; + +import com.github.hytech.storage.network.SNetworkManager; +import com.github.hytech.storage.network.device.EDeviceType; +import com.github.hytech.storage.utility.Logger; +import com.hypixel.hytale.codec.Codec; +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.EntityEventSystem; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.ecs.SwitchActiveSlotEvent; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +/** + * Sends a chat line when selecting a tagged storage block in the hotbar. + */ +public class SwitchActiveSlotEventSystem extends EntityEventSystem { + private static final String LOG_PREFIX = "[SwitchActiveSlotEventSystem] "; + + public SwitchActiveSlotEventSystem() { + super(SwitchActiveSlotEvent.class); + } + + @Override + public void handle( + int var1, + @NonNullDecl ArchetypeChunk archetypeChunk, + @NonNullDecl Store store, + @NonNullDecl CommandBuffer commandBuffer, + @NonNullDecl SwitchActiveSlotEvent event + ) { + try { + Logger.getInstance().log(LOG_PREFIX + "handle() fired"); + Ref playerRef = archetypeChunk.getReferenceTo(var1); + if (playerRef == null) { + Logger.getInstance().log(LOG_PREFIX + "playerRef is null; returning"); + return; + } + Player player = store.getComponent(playerRef, Player.getComponentType()); + if (player == null) { + Logger.getInstance().log(LOG_PREFIX + "player component is null; returning"); + return; + } + Logger.getInstance().log(LOG_PREFIX + "player resolved for ref=" + playerRef); + + ItemStack inHand = player.getInventory().getItemInHand(); + if (inHand == null || inHand.isEmpty()) { + Logger.getInstance().log(LOG_PREFIX + "item in hand is empty; returning"); + return; + } + String itemId = inHand.getItemId(); + EDeviceType deviceType = EDeviceType.fromId(itemId); + Logger.getInstance().log(LOG_PREFIX + "item in hand id=" + itemId + ", resolvedType=" + deviceType); + if (deviceType != EDeviceType.SERVER_STORAGE) { + Logger.getInstance().log(LOG_PREFIX + "item is not SERVER_STORAGE; returning"); + return; + } + + String storageId = inHand.getFromMetadataOrNull(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING); + if (storageId == null || storageId.isBlank()) { + Logger.getInstance().log(LOG_PREFIX + "storage metadata missing/blank; returning"); + return; + } + Logger.getInstance().log(LOG_PREFIX + "storageId metadata found: " + storageId); + + int storedCount = SNetworkManager.getInstance().getStoredCountByStorageId(storageId); + Logger.getInstance().log(LOG_PREFIX + "storedCount for storageId=" + storageId + " is " + storedCount); + player.sendMessage(Message.raw( + "[Hytech Storage] Loaded Storage Block - Stored: " + formatNumber(storedCount) + " item(s)" + )); + Logger.getInstance().log(LOG_PREFIX + "chat message sent"); + } catch (Exception ex) { + Logger.getInstance().log(LOG_PREFIX + "exception: " + ex.getClass().getSimpleName() + " - " + ex.getMessage()); + } + } + + private String formatNumber(int value) { + return String.format("%,d", value); + } + + @NullableDecl + @Override + public Query getQuery() { + return Query.any(); + } +} From b8dee11787c799cf1a62a6f94e591b5c210a8119 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 15:34:54 -0500 Subject: [PATCH 08/12] no storage available text hint in terminal --- .../storage/ui/TerminalInventoryPage.java | 43 +++++++++++++++++-- .../UI/Custom/Pages/HytechTerminalPage.ui | 12 ++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index bb39aa4..9925588 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -51,6 +51,8 @@ public class TerminalInventoryPage extends InteractiveCustomUIPage slots = resolveSlots(network); clampPageIndex(slots.size()); - updateSlotLabels(commandBuilder, slots); + updateSlotLabels(commandBuilder, network, slots); updatePlayerInventoryGrids(commandBuilder, ref, store); updateStorageMeter(commandBuilder, network); + updateStatusLabel(commandBuilder, network, null); commandBuilder.set("#SearchInput.Value", searchQuery); updateControlLabels(commandBuilder, slots.size()); } @@ -373,13 +376,29 @@ private void refresh(Ref playerRef, Store store, Strin Network network = resolveNetwork(); List slots = resolveSlots(network); clampPageIndex(slots.size()); - updateSlotLabels(commands, slots); + updateSlotLabels(commands, network, slots); updatePlayerInventoryGrids(commands, playerRef, store); updateStorageMeter(commands, network); + updateStatusLabel(commands, network, status); updateControlLabels(commands, slots.size()); sendUpdate(commands, null, false); } + /** + * Updates terminal status text with no-storage guidance or action feedback. + */ + private void updateStatusLabel(UICommandBuilder commands, Network network, String status) { + String message; + if (network == null || network.getServerStorages().isEmpty()) { + message = ""; + } else if (!isNullOrBlank(status)) { + message = status; + } else { + message = DEFAULT_STATUS_MESSAGE; + } + commands.set("#TerminalStatusText.Text", message); + } + /** * Updates dynamic control labels. */ @@ -811,7 +830,25 @@ private void writeNetworkTotals(Network network, Map totals) { /** * Writes display item data into the dynamic network grid. */ - private void updateSlotLabels(UICommandBuilder commands, List slots) { + private void updateSlotLabels(UICommandBuilder commands, Network network, List slots) { + boolean hasStorage = network != null && !network.getServerStorages().isEmpty(); + if (!hasStorage) { + List emptyData = new ArrayList<>(); + while (emptyData.size() < NETWORK_VISIBLE_SLOTS) { + emptyData.add(new ItemGridSlot()); + } + commands.set("#NetworkGrid.Visible", false); + commands.set("#NetworkEmptyMessage.Visible", true); + for (int slot = 0; slot < NETWORK_VISIBLE_SLOTS; slot++) { + commands.set("#NetworkSlot" + slot + ".Visible", false); + commands.set("#NetworkHoverCard" + slot + ".Visible", false); + } + commands.set("#NetworkGrid.Slots", emptyData); + return; + } + + commands.set("#NetworkGrid.Visible", true); + commands.set("#NetworkEmptyMessage.Visible", false); List slotData = new ArrayList<>(); int start = pageIndex * NETWORK_VISIBLE_SLOTS; int end = Math.min(start + NETWORK_VISIBLE_SLOTS, slots.size()); diff --git a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui index cb1b083..cdd339e 100644 --- a/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui +++ b/src/main/resources/Common/UI/Custom/Pages/HytechTerminalPage.ui @@ -103,6 +103,12 @@ Group #TerminalRoot { Group #NetworkScroll { Anchor: (Width: @NetworkGridWidth, Height: 392); LayoutMode: Full; + Label #NetworkEmptyMessage { + Anchor: (Top: 150, Left: 16, Right: 16, Height: 64); + Visible: false; + Text: "No storage available. Add storage blocks to this network."; + Style: (...$C.@DefaultLabelStyle, FontSize: 26, HorizontalAlignment: Center, RenderBold: true, TextColor: #cfdcf1); + } ItemGrid #NetworkGrid { Anchor: (Width: @NetworkGridWidth, Height: 392); SlotsPerRow: 12; @@ -1556,6 +1562,12 @@ Group #TerminalRoot { } } + Label #TerminalStatusText { + Anchor: (Top: 8, Height: 18); + Text: "Select a slot to withdraw max stack."; + Style: (...$C.@DefaultLabelStyle, FontSize: 12, HorizontalAlignment: Center, TextColor: #9fb2c7); + } + Label { Anchor: (Top: 13, Height: 20); Text: "INVENTORY"; From 3f0f6834dc0eaba50b15a75ff19dfa440df212a3 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 15:54:50 -0500 Subject: [PATCH 09/12] bug fixes --- .../storage/network/SNetworkManager.java | 31 ++---- .../world/events/BlockBreakEventSystem.java | 103 ++++++++++++------ 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java index 29535ec..39c6c4e 100644 --- a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java +++ b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java @@ -92,6 +92,13 @@ public String consumePendingDroppedStorageId() { return pendingDroppedStorageIds.pollFirst(); } + public boolean removePendingDroppedStorageId(String storageId) { + if (storageId == null || storageId.isBlank()) { + return false; + } + return pendingDroppedStorageIds.removeFirstOccurrence(storageId); + } + public int getStoredCountByStorageId(String storageId) { if (storageId == null || storageId.isBlank()) { return 0; @@ -166,12 +173,6 @@ public void onDevicePlaced(EDeviceType deviceType, Vector3i position, String sto detachedStorageCodec = detachedStorageDevices.remove(storageIdFromItem); storage.setStorageId(storageIdFromItem); } - if (detachedStorageCodec == null) { - detachedStorageCodec = consumeDetachedStorageByPosition(position); - if (detachedStorageCodec != null && detachedStorageCodec.storageId != null && !detachedStorageCodec.storageId.isBlank()) { - storage.setStorageId(detachedStorageCodec.storageId); - } - } if (detachedStorageCodec != null) { applyStorageItems(storage, detachedStorageCodec.storageItems); if (storage.getStorageId().isBlank()) { @@ -508,22 +509,4 @@ private boolean storageHasItems(DeviceServerStorage storageDevice) { return false; } - private DeviceStorageCodec consumeDetachedStorageByPosition(Vector3i position) { - if (position == null) { - return null; - } - Iterator> iterator = detachedStorageDevices.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - DeviceStorageCodec codec = entry.getValue(); - if (codec == null || codec.position == null) { - continue; - } - if (codec.position.x == position.x && codec.position.y == position.y && codec.position.z == position.z) { - iterator.remove(); - return codec; - } - } - return null; - } } diff --git a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java index 2af9f21..100378c 100644 --- a/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java +++ b/src/main/java/com/github/hytech/storage/world/events/BlockBreakEventSystem.java @@ -2,6 +2,7 @@ import com.github.hytech.storage.network.SNetworkManager; import com.github.hytech.storage.network.device.EDeviceType; +import com.github.hytech.storage.network.device.DeviceServerStorage; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Ref; @@ -10,7 +11,6 @@ import com.hypixel.hytale.component.system.EntityEventSystem; import com.hypixel.hytale.math.vector.Vector3i; import com.hypixel.hytale.protocol.GameMode; -import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent; import com.hypixel.hytale.server.core.inventory.ItemStack; @@ -21,8 +21,6 @@ import org.checkerframework.checker.nullness.compatqual.NonNullDecl; import org.checkerframework.checker.nullness.compatqual.NullableDecl; -import java.util.Locale; - /** * ECS system that reacts to block break events. */ @@ -64,69 +62,73 @@ public void handle( Ref playerRef = archetypeChunk.getReferenceTo(var1); Player player = store.getComponent(playerRef, Player.getComponentType()); boolean preserveStorageData = player == null || player.getGameMode() != GameMode.Creative; + String detachedStorageId = null; + if (deviceType == EDeviceType.SERVER_STORAGE && preserveStorageData) { + DeviceServerStorage storage = SNetworkManager.getInstance().getServerStorages().get(position); + if (storage != null && storageHasItems(storage)) { + detachedStorageId = storage.getStorageId(); + } + } SNetworkManager.getInstance().onDeviceDestroyed(position, preserveStorageData); - if (deviceType == EDeviceType.SERVER_STORAGE && preserveStorageData && player != null) { - tryAssignPendingStorageIdToInventory(playerRef, store, player); + if (player != null + && detachedStorageId != null + && !detachedStorageId.isBlank() + && tryAssignDetachedStorageIdToInventory(playerRef, store, player, detachedStorageId)) { + SNetworkManager.getInstance().removePendingDroppedStorageId(detachedStorageId); } } catch (Exception e) { } } - private void tryAssignPendingStorageIdToInventory( + /** + * Fallback for runtimes where storage block drops are inserted directly into inventory + * without passing through drop/pickup event handlers. + */ + private boolean tryAssignDetachedStorageIdToInventory( Ref playerRef, Store store, - Player player + Player player, + String detachedStorageId ) { ItemContainer inventory = player.getInventory().getCombinedEverything(); - short slotIndex = findFirstPlainStorageSlot(inventory); + short slotIndex = findPreferredPlainStorageSlot(inventory); if (slotIndex < 0) { - return; - } - - String pendingStorageId = SNetworkManager.getInstance().consumePendingDroppedStorageId(); - if (pendingStorageId == null || pendingStorageId.isBlank()) { - return; + return false; } ItemStack stack = inventory.getItemStack(slotIndex); if (stack == null || stack.isEmpty()) { - return; + return false; } if (stack.getQuantity() <= 1) { inventory.setItemStackForSlot( slotIndex, - stack.withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId) + stack.withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, detachedStorageId) ); - sendLoadedStorageMessage(player, pendingStorageId); - return; + return true; } inventory.setItemStackForSlot(slotIndex, stack.withQuantity(stack.getQuantity() - 1)); ItemStack taggedSingle = new ItemStack(stack.getItemId(), 1) - .withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, pendingStorageId); + .withMetadata(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING, detachedStorageId); SimpleItemContainer.addOrDropItemStack(store, playerRef, inventory, taggedSingle); - sendLoadedStorageMessage(player, pendingStorageId); + return true; } - private void sendLoadedStorageMessage(Player player, String storageId) { - if (player == null || storageId == null || storageId.isBlank()) { - return; - } - int storedCount = SNetworkManager.getInstance().getStoredCountByStorageId(storageId); - player.sendMessage(Message.raw( - "[Hytech Storage] Loaded storage block created (" + formatNumber(storedCount) + " item(s))." - )); - } - - private String formatNumber(int value) { - return String.format(Locale.US, "%,d", Math.max(0, value)); - } - - private short findFirstPlainStorageSlot(ItemContainer inventory) { + /** + * Chooses a deterministic plain storage slot for fallback metadata tagging. + * If ambiguous (multiple non-singleton plain stacks), skip to avoid corrupt tagging. + */ + private short findPreferredPlainStorageSlot(ItemContainer inventory) { short capacity = inventory.getCapacity(); + short firstPlain = -1; + short singleQuantityPlain = -1; + int plainCount = 0; + int singleQuantityCount = 0; + for (short i = 0; i < capacity; i++) { ItemStack stack = inventory.getItemStack(i); if (stack == null || stack.isEmpty()) { @@ -139,11 +141,40 @@ private short findFirstPlainStorageSlot(ItemContainer inventory) { if (storageId != null && !storageId.isBlank()) { continue; } - return i; + + plainCount++; + if (firstPlain < 0) { + firstPlain = i; + } + if (stack.getQuantity() == 1) { + singleQuantityCount++; + if (singleQuantityPlain < 0) { + singleQuantityPlain = i; + } + } + } + + if (singleQuantityCount == 1) { + return singleQuantityPlain; + } + if (plainCount == 1) { + return firstPlain; } return -1; } + private boolean storageHasItems(DeviceServerStorage storage) { + if (storage == null || storage.getItems() == null || storage.getItems().isEmpty()) { + return false; + } + for (Integer count : storage.getItems().values()) { + if (count != null && count > 0) { + return true; + } + } + return false; + } + /** * Declares the ECS query for this system. * From 877500a6dbdb51e4ee930a82ce2bf7bbbf42fb93 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 16:26:00 -0500 Subject: [PATCH 10/12] set recipes and crafting --- README.md | 44 +++++++++++++ .../storage/network/SNetworkManager.java | 61 +++++++++++++++++++ .../Server/Item/Items/Bench/Bench_Tech.json | 38 +++++------- .../Item/Items/Technology/Server_Rack.json | 24 +++++--- .../Item/Items/Technology/Server_Storage.json | 26 ++++---- .../Item/Items/Technology/Terminal.json | 24 +++++--- .../Server/Languages/en-US/server.lang | 4 +- 7 files changed, 167 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index d562ba2..4fb1453 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,47 @@ an example and should be removed before you release the plugin. ## Hytech Storage TODOs - Revisit terminal UI layout scaling for desktop resolutions/aspect ratios (for example 1080p, 1440p, ultrawide) and reduce fixed-size assumptions where possible. + +## Hytech Storage Recipes (Quick Reference) + +All Hytech items below currently craft at: +- `Bench_Tech` crafts at: `Workbench` (`Workbench_Crafting`, Tier 1) +- `Terminal`, `Server_Rack`, `Server_Storage` craft at: `Techbench` (`Hytech_Storage`, Tier 1) + +### Bench_Tech (Tech Workbench) +- Time: `5s` +- Inputs: + - `Ore_Iron` x18 + - `Ore_Copper` x16 + - `Ore_Gold` x10 + - `Ore_Silver` x8 + - `Ore_Thorium` x6 +- Bench Categories: + - `Hytech_Storage` (single category) + +### Terminal +- Time: `3s` +- Inputs: + - `Ore_Iron` x8 + - `Ore_Copper` x8 + - `Ore_Gold` x4 + - `Ore_Silver` x2 + - `Ore_Thorium` x1 + +### Server_Rack +- Time: `4s` +- Inputs: + - `Ore_Iron` x10 + - `Ore_Copper` x10 + - `Ore_Gold` x5 + - `Ore_Silver` x3 + - `Ore_Thorium` x2 + +### Server_Storage +- Time: `5s` +- Inputs: + - `Ore_Iron` x14 + - `Ore_Copper` x14 + - `Ore_Gold` x7 + - `Ore_Silver` x5 + - `Ore_Thorium` x3 diff --git a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java index 39c6c4e..5ecf833 100644 --- a/src/main/java/com/github/hytech/storage/network/SNetworkManager.java +++ b/src/main/java/com/github/hytech/storage/network/SNetworkManager.java @@ -194,6 +194,7 @@ public void onDevicePlaced(EDeviceType deviceType, Vector3i position, String sto serverRacks.put(position, serverRack); } + cleanupDetachedStorageState(); if (!restoringFromSaveFile) { StateManager.getInstance().save(); } @@ -280,6 +281,7 @@ && storageHasItems(storageDevice)) { case SERVER_RACK -> serverRacks.remove(position); } + cleanupDetachedStorageState(); if (!restoringFromSaveFile) { StateManager.getInstance().save(); } @@ -340,6 +342,8 @@ public void restoreIntoNetworkManager(NetworkManagerCodec networkManagerCodec) { } } + cleanupDetachedStorageState(); + restoringFromSaveFile = false; Logger.getInstance().logGame("restored networks: " + networks.size()); @@ -509,4 +513,61 @@ private boolean storageHasItems(DeviceServerStorage storageDevice) { return false; } + /** + * Conservatively prunes stale detached storage bookkeeping. + * Keeps all detached entries that contain any items to avoid losing recoverable data. + */ + private void cleanupDetachedStorageState() { + // Build set of pending ids that are valid references. + Set validPending = new HashSet<>(); + Iterator pendingIterator = pendingDroppedStorageIds.iterator(); + while (pendingIterator.hasNext()) { + String id = pendingIterator.next(); + if (id == null || id.isBlank() || !detachedStorageDevices.containsKey(id)) { + pendingIterator.remove(); + continue; + } + validPending.add(id); + } + + // Remove malformed/empty detached entries that are not pending. + Iterator> detachedIterator = detachedStorageDevices.entrySet().iterator(); + while (detachedIterator.hasNext()) { + Map.Entry entry = detachedIterator.next(); + String storageId = entry.getKey(); + DeviceStorageCodec codec = entry.getValue(); + + if (storageId == null || storageId.isBlank() || codec == null) { + detachedIterator.remove(); + continue; + } + + if (codec.storageId == null || codec.storageId.isBlank()) { + codec.storageId = storageId; + } + + if (detachedStorageHasItems(codec)) { + continue; + } + + if (validPending.contains(storageId)) { + continue; + } + + detachedIterator.remove(); + } + } + + private boolean detachedStorageHasItems(DeviceStorageCodec codec) { + if (codec == null || codec.storageItems == null || codec.storageItems.length == 0) { + return false; + } + for (StorageItemCodec item : codec.storageItems) { + if (item != null && item.count > 0) { + return true; + } + } + return false; + } + } diff --git a/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json b/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json index f500347..80aa334 100644 --- a/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json +++ b/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json @@ -8,23 +8,27 @@ "Furniture.Benches" ], "Recipe": { - "TimeSeconds": 3, + "TimeSeconds": 5, "Input": [ { - "ResourceTypeId": "Rock", - "Quantity": 20 + "ItemId": "Ore_Iron", + "Quantity": 18 }, { - "ItemId": "Ingredient_Bar_Gold", - "Quantity": 5 + "ItemId": "Ore_Copper", + "Quantity": 16 }, { - "ItemId": "Ingredient_Sac_Venom", + "ItemId": "Ore_Gold", "Quantity": 10 }, { - "ItemId": "Ingredient_Bone_Fragment", - "Quantity": 10 + "ItemId": "Ore_Silver", + "Quantity": 8 + }, + { + "ItemId": "Ore_Thorium", + "Quantity": 6 } ], "BenchRequirement": [ @@ -60,22 +64,12 @@ "BenchUpgradeCompletedSoundEventId": "SFX_Workbench_Upgrade_Complete_Default", "Categories": [ { - "Id": "Alchemy_Potions", - "Icon": "Icons/CraftingCategories/Alchemy/Combat_Potions.png", - "Name": "server.benchCategories.combatPotions" - }, - { - "Id": "Alchemy_Potions_Misc", - "Icon": "Icons/CraftingCategories/Alchemy/Misc_Potions.png", - "Name": "server.benchCategories.miscPotions" - }, - { - "Id": "Alchemy_Bombs", - "Icon": "Icons/CraftingCategories/Alchemy/Bombs.png", - "Name": "server.benchCategories.bombs" + "Id": "Hytech_Storage", + "Icon": "Icons/CraftingCategories/Workbench/Processing.png", + "Name": "server.benchCategories.hytechStorage" } ], - "Id": "Alchemybench", + "Id": "Techbench", "TierLevels": [ { "CraftingTimeReductionModifier": 0.0, diff --git a/src/main/resources/Server/Item/Items/Technology/Server_Rack.json b/src/main/resources/Server/Item/Items/Technology/Server_Rack.json index c035673..3b2fde2 100644 --- a/src/main/resources/Server/Item/Items/Technology/Server_Rack.json +++ b/src/main/resources/Server/Item/Items/Technology/Server_Rack.json @@ -8,31 +8,35 @@ "Blocks.Decoration" ], "Recipe": { - "TimeSeconds": 3, + "TimeSeconds": 4, "Input": [ { - "ResourceTypeId": "Rock", - "Quantity": 20 + "ItemId": "Ore_Iron", + "Quantity": 10 + }, + { + "ItemId": "Ore_Copper", + "Quantity": 10 }, { - "ItemId": "Ingredient_Bar_Gold", + "ItemId": "Ore_Gold", "Quantity": 5 }, { - "ItemId": "Ingredient_Sac_Venom", - "Quantity": 10 + "ItemId": "Ore_Silver", + "Quantity": 3 }, { - "ItemId": "Ingredient_Bone_Fragment", - "Quantity": 10 + "ItemId": "Ore_Thorium", + "Quantity": 2 } ], "BenchRequirement": [ { - "Id": "Workbench", + "Id": "Techbench", "Type": "Crafting", "Categories": [ - "Workbench_Crafting" + "Hytech_Storage" ], "RequiredTierLevel": 1 } diff --git a/src/main/resources/Server/Item/Items/Technology/Server_Storage.json b/src/main/resources/Server/Item/Items/Technology/Server_Storage.json index 451a452..1ad7300 100644 --- a/src/main/resources/Server/Item/Items/Technology/Server_Storage.json +++ b/src/main/resources/Server/Item/Items/Technology/Server_Storage.json @@ -8,31 +8,35 @@ "Blocks.Decoration" ], "Recipe": { - "TimeSeconds": 3, + "TimeSeconds": 5, "Input": [ { - "ResourceTypeId": "Rock", - "Quantity": 20 + "ItemId": "Ore_Iron", + "Quantity": 14 }, { - "ItemId": "Ingredient_Bar_Gold", - "Quantity": 5 + "ItemId": "Ore_Copper", + "Quantity": 14 + }, + { + "ItemId": "Ore_Gold", + "Quantity": 7 }, { - "ItemId": "Ingredient_Sac_Venom", - "Quantity": 10 + "ItemId": "Ore_Silver", + "Quantity": 5 }, { - "ItemId": "Ingredient_Bone_Fragment", - "Quantity": 10 + "ItemId": "Ore_Thorium", + "Quantity": 3 } ], "BenchRequirement": [ { - "Id": "Workbench", + "Id": "Techbench", "Type": "Crafting", "Categories": [ - "Workbench_Crafting" + "Hytech_Storage" ], "RequiredTierLevel": 1 } diff --git a/src/main/resources/Server/Item/Items/Technology/Terminal.json b/src/main/resources/Server/Item/Items/Technology/Terminal.json index 8e905c0..43f1df5 100644 --- a/src/main/resources/Server/Item/Items/Technology/Terminal.json +++ b/src/main/resources/Server/Item/Items/Technology/Terminal.json @@ -11,28 +11,32 @@ "TimeSeconds": 3, "Input": [ { - "ResourceTypeId": "Rock", - "Quantity": 20 + "ItemId": "Ore_Iron", + "Quantity": 8 }, { - "ItemId": "Ingredient_Bar_Gold", - "Quantity": 5 + "ItemId": "Ore_Copper", + "Quantity": 8 }, { - "ItemId": "Ingredient_Sac_Venom", - "Quantity": 10 + "ItemId": "Ore_Gold", + "Quantity": 4 }, { - "ItemId": "Ingredient_Bone_Fragment", - "Quantity": 10 + "ItemId": "Ore_Silver", + "Quantity": 2 + }, + { + "ItemId": "Ore_Thorium", + "Quantity": 1 } ], "BenchRequirement": [ { - "Id": "Workbench", + "Id": "Techbench", "Type": "Crafting", "Categories": [ - "Workbench_Crafting" + "Hytech_Storage" ], "RequiredTierLevel": 1 } diff --git a/src/main/resources/Server/Languages/en-US/server.lang b/src/main/resources/Server/Languages/en-US/server.lang index 472e7b8..8219969 100644 --- a/src/main/resources/Server/Languages/en-US/server.lang +++ b/src/main/resources/Server/Languages/en-US/server.lang @@ -8,4 +8,6 @@ items.Server_Storage.name = Server Storage items.Server_Storage.description = Expands a network's storage capacity. items.Terminal.name = Terminal -items.Terminal.description = Access a network's inventory. \ No newline at end of file +items.Terminal.description = Access a network's inventory. + +benchCategories.hytechStorage = Hytech Storage From 840d29ff212178028d1808b9415b80b8aa7741f6 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 16:35:24 -0500 Subject: [PATCH 11/12] small bug fixes --- .../hytech/storage/network/Network.java | 33 +++++++++++++++--- .../storage/ui/TerminalInventoryPage.java | 34 +++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/github/hytech/storage/network/Network.java b/src/main/java/com/github/hytech/storage/network/Network.java index d96d32a..f65e03b 100644 --- a/src/main/java/com/github/hytech/storage/network/Network.java +++ b/src/main/java/com/github/hytech/storage/network/Network.java @@ -161,13 +161,36 @@ private void detectSeveredNetworks() { if (groups.size() <= 1) return; Set keep = groups.getFirst(); - - devices.keySet().removeIf(pos -> { - DeviceBase d = devices.get(pos); - return d != null && !keep.contains(d); - }); + Iterator> iterator = devices.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (!keep.contains(entry.getValue())) { + iterator.remove(); + } + } + rebuildTypedIndexesFromDevices(); List> movedGroups = groups.subList(1, groups.size()); SNetworkManager.getInstance().createSeveredNetwork(movedGroups); } + + /** + * Rebuilds per-type device indexes from the canonical devices map. + * This is required after split operations that directly mutate {@code devices}. + */ + private void rebuildTypedIndexesFromDevices() { + serverRacks.clear(); + serverStorages.clear(); + terminals.clear(); + + for (DeviceBase device : devices.values()) { + switch (device) { + case DeviceServerStorage serverStorage -> serverStorages.put(serverStorage.getPosition(), serverStorage); + case DeviceServerRack serverRack -> serverRacks.put(serverRack.getPosition(), serverRack); + case DeviceTerminal terminal -> terminals.put(terminal.getPosition(), terminal); + default -> { + } + } + } + } } diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index 9925588..68da501 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -389,7 +389,7 @@ private void refresh(Ref playerRef, Store store, Strin */ private void updateStatusLabel(UICommandBuilder commands, Network network, String status) { String message; - if (network == null || network.getServerStorages().isEmpty()) { + if (!hasStorageDevices(network)) { message = ""; } else if (!isNullOrBlank(status)) { message = status; @@ -716,7 +716,7 @@ private int addIntoNetworkStorage(Network network, String itemId, int requested) Map totals = getNetworkTotals(network); int used = totals.values().stream().mapToInt(Integer::intValue).sum(); - int max = network.getServerStorages().size() * DeviceServerStorage.getStorageMax(); + int max = resolveNetworkStorages(network).size() * DeviceServerStorage.getStorageMax(); int free = Math.max(0, max - used); int accepted = Math.min(requested, free); if (accepted <= 0) { @@ -791,7 +791,7 @@ private List getFilteredItems(Network network) { */ private Map getNetworkTotals(Network network) { Map totals = new LinkedHashMap<>(); - for (DeviceServerStorage storage : network.getServerStorages().values()) { + for (DeviceServerStorage storage : resolveNetworkStorages(network)) { for (Map.Entry entry : storage.getItems().entrySet()) { totals.merge(entry.getKey(), entry.getValue(), Integer::sum); } @@ -803,8 +803,9 @@ private Map getNetworkTotals(Network network) { * Writes merged totals back into storage blocks in first-added order. */ private void writeNetworkTotals(Network network, Map totals) { + List storages = resolveNetworkStorages(network); Map remaining = new LinkedHashMap<>(totals); - for (DeviceServerStorage storage : network.getServerStorages().values()) { + for (DeviceServerStorage storage : storages) { Map next = new LinkedHashMap<>(); int used = 0; Iterator> iterator = remaining.entrySet().iterator(); @@ -831,7 +832,7 @@ private void writeNetworkTotals(Network network, Map totals) { * Writes display item data into the dynamic network grid. */ private void updateSlotLabels(UICommandBuilder commands, Network network, List slots) { - boolean hasStorage = network != null && !network.getServerStorages().isEmpty(); + boolean hasStorage = hasStorageDevices(network); if (!hasStorage) { List emptyData = new ArrayList<>(); while (emptyData.size() < NETWORK_VISIBLE_SLOTS) { @@ -1023,7 +1024,7 @@ private void updateStorageMeter(UICommandBuilder commands, Network network) { if (network != null) { Map totals = getNetworkTotals(network); used = totals.values().stream().mapToInt(Integer::intValue).sum(); - max = network.getServerStorages().size() * DeviceServerStorage.getStorageMax(); + max = resolveNetworkStorages(network).size() * DeviceServerStorage.getStorageMax(); } double ratio = max > 0 ? Math.min(1.0, (double) used / max) : 0.0; @@ -1076,6 +1077,27 @@ private String formatWithCommas(int value) { return String.format(Locale.US, "%,d", Math.max(0, value)); } + /** + * Resolves storage devices currently associated with the provided network. + * Uses the global registry filtered by device.network to avoid stale local indexes. + */ + private List resolveNetworkStorages(Network network) { + List storages = new ArrayList<>(); + if (network == null) { + return storages; + } + for (DeviceServerStorage storage : SNetworkManager.getInstance().getServerStorages().values()) { + if (storage.getNetwork() == network) { + storages.add(storage); + } + } + return storages; + } + + private boolean hasStorageDevices(Network network) { + return !resolveNetworkStorages(network).isEmpty(); + } + /** * Resolves the network connected to this terminal page's terminal position. */ From 39b0f0a679b2a9aaafc2a2077d4264ce79a2a392 Mon Sep 17 00:00:00 2001 From: James Alphonse Date: Sun, 15 Feb 2026 23:49:53 -0500 Subject: [PATCH 12/12] various bug fixes. --- AGENTS.md | 5 +++ README.md | 40 +++++++++---------- SAVE_MIGRATIONS.md | 26 ++++++++++++ build.gradle | 17 ++++++++ gradle.properties | 10 ++++- .../storage/ui/TerminalInventoryPage.java | 18 ++++++--- .../Server/Item/Items/Bench/Bench_Tech.json | 10 ++--- .../Item/Items/Technology/Server_Rack.json | 10 ++--- .../Item/Items/Technology/Server_Storage.json | 10 ++--- .../Item/Items/Technology/Terminal.json | 10 ++--- 10 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 SAVE_MIGRATIONS.md diff --git a/AGENTS.md b/AGENTS.md index e744d8f..074313d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,3 +51,8 @@ This repo is a Hytale storage mod inspired by Minecraft storage mods (e.g., Refi ## Hytale API Notes - When a new Hytale API (PBI) type is encountered in this repo, add a short explanation to `HT_PBI_OVERVIEW.md`. + +## Save Data Migrations +- If any change modifies the save-data structure (for example codecs, field names, required fields, enum/type ids, or layout of `NetworkManagerState.json`), always implement a migration in code in the same change. +- Every save-data migration must be recorded in `SAVE_MIGRATIONS.md`. +- Do not merge save-structure changes without both: (1) code migration path and (2) migration entry documentation. diff --git a/README.md b/README.md index 4fb1453..cd77e6b 100644 --- a/README.md +++ b/README.md @@ -110,37 +110,37 @@ All Hytech items below currently craft at: ### Bench_Tech (Tech Workbench) - Time: `5s` - Inputs: - - `Ore_Iron` x18 - - `Ore_Copper` x16 - - `Ore_Gold` x10 - - `Ore_Silver` x8 - - `Ore_Thorium` x6 + - `Ingredient_Bar_Iron` x18 + - `Ingredient_Bar_Copper` x16 + - `Ingredient_Bar_Gold` x10 + - `Ingredient_Bar_Silver` x8 + - `Ingredient_Bar_Thorium` x6 - Bench Categories: - `Hytech_Storage` (single category) ### Terminal - Time: `3s` - Inputs: - - `Ore_Iron` x8 - - `Ore_Copper` x8 - - `Ore_Gold` x4 - - `Ore_Silver` x2 - - `Ore_Thorium` x1 + - `Ingredient_Bar_Iron` x8 + - `Ingredient_Bar_Copper` x8 + - `Ingredient_Bar_Gold` x4 + - `Ingredient_Bar_Silver` x2 + - `Ingredient_Bar_Thorium` x1 ### Server_Rack - Time: `4s` - Inputs: - - `Ore_Iron` x10 - - `Ore_Copper` x10 - - `Ore_Gold` x5 - - `Ore_Silver` x3 - - `Ore_Thorium` x2 + - `Ingredient_Bar_Iron` x10 + - `Ingredient_Bar_Copper` x10 + - `Ingredient_Bar_Gold` x5 + - `Ingredient_Bar_Silver` x3 + - `Ingredient_Bar_Thorium` x2 ### Server_Storage - Time: `5s` - Inputs: - - `Ore_Iron` x14 - - `Ore_Copper` x14 - - `Ore_Gold` x7 - - `Ore_Silver` x5 - - `Ore_Thorium` x3 + - `Ingredient_Bar_Iron` x14 + - `Ingredient_Bar_Copper` x14 + - `Ingredient_Bar_Gold` x7 + - `Ingredient_Bar_Silver` x5 + - `Ingredient_Bar_Thorium` x3 diff --git a/SAVE_MIGRATIONS.md b/SAVE_MIGRATIONS.md new file mode 100644 index 0000000..e84da28 --- /dev/null +++ b/SAVE_MIGRATIONS.md @@ -0,0 +1,26 @@ +# Save Data Migrations + +Canonical migration ledger for save-state changes affecting: +- `run/hytech_storage/NetworkManagerState.json` +- codec models under `src/main/java/com/github/hytech/storage/state/codec/` +- load/restore logic in `src/main/java/com/github/hytech/storage/state/StateManager.java` + and `src/main/java/com/github/hytech/storage/network/SNetworkManager.java` + +## Rules +- Any save-structure change must ship with a migration path in code. +- Any save-structure change must add an entry to this file in the same commit. +- Entries are append-only. + +## Entry Template +Use this format for each migration: + +### YYYY-MM-DD - MIG-XXX - Short Title +- Author: name +- Affected versions: from -> to +- Breaking: yes/no +- Summary: what changed in the save format +- Migration path: where migration logic was added +- Rollback notes: how old/new saves behave if reverted + +## Migrations +No save-data migrations recorded yet. diff --git a/build.gradle b/build.gradle index 8df6db3..e9e14b1 100644 --- a/build.gradle +++ b/build.gradle @@ -73,9 +73,26 @@ tasks.register('updatePluginManifest') { } } +// Optional comma-separated Ant-style patterns for files that should not be +// included in the built plugin jar (for example: Common/UI/**, **/*.psd). +def jarExcludes = ((findProperty('jar_excludes') ?: '') as String) + .split(',') + .collect { it.trim() } + .findAll { !it.isEmpty() } + // Makes sure the plugin manifest is up to date. tasks.named('processResources') { dependsOn 'updatePluginManifest' + if (!jarExcludes.isEmpty()) { + exclude(jarExcludes) + } +} + +// Ensure excluded files are also filtered at the jar packaging step. +tasks.named('jar') { + if (!jarExcludes.isEmpty()) { + exclude(jarExcludes) + } } def createServerRunArguments(String srcDir) { diff --git a/gradle.properties b/gradle.properties index 2688f5e..918175d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,8 +25,16 @@ patchline=release # to the development server manually. load_user_mods=false +# Optional comma-separated Ant-style file patterns to exclude from the built +# plugin jar. +# Includes common dev/editor/junk paths by default (defensive; many are top-level +# and not part of sourceSets, but are blocked here to prevent accidental inclusion). +jar_excludes=agents/**,skills/**,.idea/**,.gradle/**,run/**,build/**,**/*.psd,**/*.bak,**/*.tmp,**/*~,**/.DS_Store,**/Thumbs.db,AGENTS.md,HT_PBI_OVERVIEW.md,README.md + # If Hytale was installed to a custom location, you must set the home path # manually. You may also want to use a custom path if you are building in # a non-standard environment like a build server. The home path should # the folder that contains the install and UserData folder. - hytale_home=A:/Hytale \ No newline at end of file + hytale_home=A:/Hytale + +org.gradle.java.home=C:/Program Files/Java/jdk-25.0.2 \ No newline at end of file diff --git a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java index 68da501..2cafb7a 100644 --- a/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java +++ b/src/main/java/com/github/hytech/storage/ui/TerminalInventoryPage.java @@ -2,7 +2,6 @@ import com.github.hytech.storage.network.Network; import com.github.hytech.storage.network.SNetworkManager; -import com.github.hytech.storage.network.device.EDeviceType; import com.github.hytech.storage.network.device.DeviceServerStorage; import com.github.hytech.storage.network.device.DeviceTerminal; import com.github.hytech.storage.state.StateManager; @@ -1007,12 +1006,19 @@ private ItemStack toPlayerGridDisplayStack(ItemStack stack) { if (stack == null || stack.isEmpty()) { return stack; } - if (SNetworkManager.STORAGE_ID_METADATA_KEY != null - && stack.getFromMetadataOrNull(SNetworkManager.STORAGE_ID_METADATA_KEY, Codec.STRING) != null - && EDeviceType.fromId(stack.getItemId()) == EDeviceType.SERVER_STORAGE) { - return new ItemStack(stack.getItemId(), stack.getQuantity()); + + // Use plain display stacks in the custom page to avoid serializing problematic metadata. + String itemId = stack.getItemId(); + if (itemId == null || itemId.isBlank()) { + return ItemStack.EMPTY; + } + + int quantity = Math.max(1, stack.getQuantity()); + try { + return new ItemStack(itemId, quantity); + } catch (Exception ignored) { + return ItemStack.EMPTY; } - return stack; } /** diff --git a/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json b/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json index 80aa334..7e5c4de 100644 --- a/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json +++ b/src/main/resources/Server/Item/Items/Bench/Bench_Tech.json @@ -11,23 +11,23 @@ "TimeSeconds": 5, "Input": [ { - "ItemId": "Ore_Iron", + "ItemId": "Ingredient_Bar_Iron", "Quantity": 18 }, { - "ItemId": "Ore_Copper", + "ItemId": "Ingredient_Bar_Copper", "Quantity": 16 }, { - "ItemId": "Ore_Gold", + "ItemId": "Ingredient_Bar_Gold", "Quantity": 10 }, { - "ItemId": "Ore_Silver", + "ItemId": "Ingredient_Bar_Silver", "Quantity": 8 }, { - "ItemId": "Ore_Thorium", + "ItemId": "Ingredient_Bar_Thorium", "Quantity": 6 } ], diff --git a/src/main/resources/Server/Item/Items/Technology/Server_Rack.json b/src/main/resources/Server/Item/Items/Technology/Server_Rack.json index 3b2fde2..be9a881 100644 --- a/src/main/resources/Server/Item/Items/Technology/Server_Rack.json +++ b/src/main/resources/Server/Item/Items/Technology/Server_Rack.json @@ -11,23 +11,23 @@ "TimeSeconds": 4, "Input": [ { - "ItemId": "Ore_Iron", + "ItemId": "Ingredient_Bar_Iron", "Quantity": 10 }, { - "ItemId": "Ore_Copper", + "ItemId": "Ingredient_Bar_Copper", "Quantity": 10 }, { - "ItemId": "Ore_Gold", + "ItemId": "Ingredient_Bar_Gold", "Quantity": 5 }, { - "ItemId": "Ore_Silver", + "ItemId": "Ingredient_Bar_Silver", "Quantity": 3 }, { - "ItemId": "Ore_Thorium", + "ItemId": "Ingredient_Bar_Thorium", "Quantity": 2 } ], diff --git a/src/main/resources/Server/Item/Items/Technology/Server_Storage.json b/src/main/resources/Server/Item/Items/Technology/Server_Storage.json index 1ad7300..34e0382 100644 --- a/src/main/resources/Server/Item/Items/Technology/Server_Storage.json +++ b/src/main/resources/Server/Item/Items/Technology/Server_Storage.json @@ -11,23 +11,23 @@ "TimeSeconds": 5, "Input": [ { - "ItemId": "Ore_Iron", + "ItemId": "Ingredient_Bar_Iron", "Quantity": 14 }, { - "ItemId": "Ore_Copper", + "ItemId": "Ingredient_Bar_Copper", "Quantity": 14 }, { - "ItemId": "Ore_Gold", + "ItemId": "Ingredient_Bar_Gold", "Quantity": 7 }, { - "ItemId": "Ore_Silver", + "ItemId": "Ingredient_Bar_Silver", "Quantity": 5 }, { - "ItemId": "Ore_Thorium", + "ItemId": "Ingredient_Bar_Thorium", "Quantity": 3 } ], diff --git a/src/main/resources/Server/Item/Items/Technology/Terminal.json b/src/main/resources/Server/Item/Items/Technology/Terminal.json index 43f1df5..9b435bf 100644 --- a/src/main/resources/Server/Item/Items/Technology/Terminal.json +++ b/src/main/resources/Server/Item/Items/Technology/Terminal.json @@ -11,23 +11,23 @@ "TimeSeconds": 3, "Input": [ { - "ItemId": "Ore_Iron", + "ItemId": "Ingredient_Bar_Iron", "Quantity": 8 }, { - "ItemId": "Ore_Copper", + "ItemId": "Ingredient_Bar_Copper", "Quantity": 8 }, { - "ItemId": "Ore_Gold", + "ItemId": "Ingredient_Bar_Gold", "Quantity": 4 }, { - "ItemId": "Ore_Silver", + "ItemId": "Ingredient_Bar_Silver", "Quantity": 2 }, { - "ItemId": "Ore_Thorium", + "ItemId": "Ingredient_Bar_Thorium", "Quantity": 1 } ],