/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.client.gui.controller.game; import de.lessvoid.nifty.Nifty; import de.lessvoid.nifty.NiftyEventSubscriber; import de.lessvoid.nifty.controls.DraggableDragCanceledEvent; import de.lessvoid.nifty.controls.DraggableDragStartedEvent; import de.lessvoid.nifty.controls.DroppableDroppedEvent; import de.lessvoid.nifty.elements.Element; import de.lessvoid.nifty.elements.events.NiftyMouseMovedEvent; import de.lessvoid.nifty.elements.events.NiftyMousePrimaryMultiClickedEvent; import de.lessvoid.nifty.render.NiftyImage; import de.lessvoid.nifty.screen.Screen; import de.lessvoid.nifty.screen.ScreenController; import de.lessvoid.nifty.tools.SizeValue; import illarion.client.IllaClient; import illarion.client.graphics.FontLoader; import illarion.client.gui.ContainerGui; import illarion.client.gui.EntitySlickRenderImage; import illarion.client.gui.Tooltip; import illarion.client.gui.controller.game.NumberSelectPopupHandler.Callback; import illarion.client.net.client.CloseShowcaseCmd; import illarion.client.resources.ItemFactory; import illarion.client.resources.data.ItemTemplate; import illarion.client.util.Lang; import illarion.client.util.LookAtTracker; import illarion.client.util.UpdateTask; import illarion.client.world.World; import illarion.client.world.interactive.InteractionManager; import illarion.client.world.items.ContainerSlot; import illarion.client.world.items.ItemContainer; import illarion.client.world.items.MerchantItem; import illarion.client.world.items.MerchantList; import illarion.common.types.ItemCount; import illarion.common.types.ItemId; import illarion.common.types.Rectangle; import org.illarion.engine.GameContainer; import org.illarion.engine.graphic.Font; import org.illarion.engine.input.Button; import org.illarion.engine.input.Input; import org.illarion.engine.input.Key; import org.illarion.nifty.controls.InventorySlot; import org.illarion.nifty.controls.InventorySlot.MerchantBuyLevel; import org.illarion.nifty.controls.ItemContainerCloseEvent; import org.illarion.nifty.controls.itemcontainer.builder.ItemContainerBuilder; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This handler that care for properly managing the displaying of containers on the game screen. * * @author Martin Karing <nitram@illarion.org> */ public final class ContainerHandler implements ContainerGui, ScreenController { /** * This class is used as drag end operation and used to move a object that was dragged out of the inventory back in * so the server can send the commands to clean everything up. * * @author Martin Karing <nitram@illarion.org> */ private static class EndOfDragOperation implements Runnable { /** * The inventory slot that requires the reset. */ @Nonnull private final InventorySlot invSlot; /** * Create a new instance of this class and set the effected elements. * * @param slot the inventory slot to reset */ EndOfDragOperation(@Nonnull InventorySlot slot) { invSlot = slot; } /** * Execute this operation. */ @Override public void run() { invSlot.retrieveDraggable(); } } private final class UpdateContainerTask implements UpdateTask { @Nonnull private final ItemContainer itemContainer; UpdateContainerTask(@Nonnull ItemContainer container) { itemContainer = container; } @Override public void onUpdateGame(@Nonnull GameContainer container, int delta) { if (!isContainerCreated(itemContainer.getContainerId())) { createNewContainer(itemContainer); } updateContainer(itemContainer); } } /** * The pattern to fetch the ID of a slot name. */ @Nonnull private static final Pattern slotPattern = Pattern.compile("slot([0-9]+)"); /** * The pattern to fetch the ID of a container name. */ @Nonnull private static final Pattern containerPattern = Pattern.compile("container([0-9]+)"); @Nonnull private static final Logger log = LoggerFactory.getLogger(ContainerHandler.class); /** * The Nifty-GUI instance that is handling the GUI display currently. */ @Nullable private Nifty activeNifty; /** * The screen that takes care for the display currently. */ @Nullable private Screen activeScreen; /** * The select popup handler that is used to receive money input from the user. */ @Nonnull private final NumberSelectPopupHandler numberSelect; /** * The tooltip handler that is used to show the tooltips of this container. */ @Nonnull private final TooltipHandler tooltipHandler; /** * The list of item containers that are currently displayed. */ @Nonnull private final Map<Integer, org.illarion.nifty.controls.ItemContainer> itemContainerMap; /** * Creating new item containers in a extremely expensive operation. It is in all cases better to reuse existing * instances of the containers if possible. */ @Nonnull private final Collection<org.illarion.nifty.controls.ItemContainer> itemContainerCache; /** * The task that is executed to update the merchant overlays. */ @Nonnull private final UpdateTask updateMerchantOverlays = (container, delta) -> updateAllMerchantOverlays(); /** * The input system that is used to query the state of the keyboard. */ @Nonnull private final Input input; /** * Constructor of this handler. * * @param numberSelectPopupHandler the number select handler * @param tooltip the tooltip handler */ public ContainerHandler(@Nonnull Input input, @Nonnull NumberSelectPopupHandler numberSelectPopupHandler, @Nonnull TooltipHandler tooltip) { itemContainerMap = new HashMap<>(); itemContainerCache = new ArrayList<>(); numberSelect = numberSelectPopupHandler; tooltipHandler = tooltip; this.input = input; } /** * This event is received in case a container is closed. * * @param topic the topic of the event * @param data the event data */ @NiftyEventSubscriber(pattern = ".*container[0-9]+.*") public void onItemContainerClose(@Nonnull String topic, @Nonnull ItemContainerCloseEvent data) { int containerId = getContainerId(topic); World.getPlayer().removeContainer(containerId); if (isContainerCreated(containerId)) { removeItemContainer(containerId); World.getNet().sendCommand(new CloseShowcaseCmd(containerId)); } } /** * Check if a container with a specified ID is already created. * * @param containerId the container ID * @return {@code true} in case the container is already created */ private boolean isContainerCreated(int containerId) { return itemContainerMap.containsKey(containerId); } private void removeItemContainer(int id) { org.illarion.nifty.controls.ItemContainer container = itemContainerMap.remove(id); if (container == null) { return; } String prefix = getPrefix(id); IllaClient.getCfg().set(prefix + "DisplayPosX", container.getElement().getConstraintX().toString()); IllaClient.getCfg().set(prefix + "DisplayPosY", container.getElement().getConstraintY().toString()); container.closeWindow(); itemContainerCache.add(container); } /** * This event is received in case the dragging of a item is canceled. * * @param topic the topic of the event * @param data the event data */ @NiftyEventSubscriber(pattern = ".*container[0-9]+.*slot[0-9]+.*") public void cancelDragging(String topic, DraggableDragCanceledEvent data) { World.getInteractionManager().cancelDragging(); } /** * This event is received in case the user clicks into the container. * * @param topic the topic of the event * @param data the event data */ @NiftyEventSubscriber(pattern = ".*container[0-9]+.*slot[0-9]+.*") public void onDoubleClickInContainer(@Nonnull String topic, @Nonnull NiftyMousePrimaryMultiClickedEvent data) { int slotId = getSlotId(topic); int containerId = getContainerId(topic); ItemContainer container = World.getPlayer().getContainer(containerId); if (container == null) { log.error("Used container {} does not exist.", containerId); return; } ContainerSlot slot = container.getSlot(slotId); if (!slot.containsItem()) { return; } if (data.getClickCount() == 2) { if (World.getPlayer().hasMerchantList()) { slot.getInteractive().sell(); } else { if (slot.getItemTemplate().getItemInfo().isContainer()) { slot.getInteractive().openContainer(); } else { slot.getInteractive().use(); } } } } /** * Get the slot ID that is stored in the ID a element. * * @param key the key of the element * @return the extracted ID */ private static int getSlotId(@Nonnull CharSequence key) { Matcher matcher = slotPattern.matcher(key); if (!matcher.find()) { return -1; } if (matcher.groupCount() == 0) { return -1; } return Integer.parseInt(matcher.group(1)); } /** * Get the container ID that is stored in the ID a element. * * @param key the key of the element * @return the extracted ID */ private int getContainerId(@Nonnull CharSequence key) { Matcher matcher = containerPattern.matcher(key); if (!matcher.find()) { return -1; } if (matcher.groupCount() == 0) { return -1; } int nameId = Integer.parseInt(matcher.group(1)); String fullName = "container" + nameId; for (Entry<Integer, org.illarion.nifty.controls.ItemContainer> entry : itemContainerMap.entrySet()) { Element containerElement = entry.getValue().getElement(); if (containerElement != null) { String containerId = containerElement.getId(); if ((containerId != null) && containerId.contains(fullName)) { return entry.getKey(); } } } return -1; } /** * This event is received in case the user drags the item away from its slot. * * @param topic the topic of the event * @param data the event data */ @NiftyEventSubscriber(pattern = ".*container[0-9]+.*slot[0-9]+.*") public void dragFrom(@Nonnull String topic, @Nonnull DraggableDragStartedEvent data) { int slotId = getSlotId(topic); int containerId = getContainerId(topic); Element slot = data.getSource().getElement(); Element parentSlot = (slot == null) ? null : slot.getParent(); InventorySlot invSlot = (parentSlot == null) ? null : parentSlot.getNiftyControl(InventorySlot.class); if (invSlot != null) { InteractionManager iManager = World.getInteractionManager(); Runnable endOp = new EndOfDragOperation(invSlot); iManager.notifyDraggingContainer(containerId, slotId, endOp); } } /** * This event is received in case the user drops the item away into a slot. * * @param topic the topic of the event * @param data the event data */ @NiftyEventSubscriber(pattern = ".*container[0-9]+.*slot[0-9]+.*") public void dropIn(@Nonnull String topic, @Nonnull DroppableDroppedEvent data) { int slotId = getSlotId(topic); int containerId = getContainerId(topic); InteractionManager iManager = World.getInteractionManager(); ItemCount amount = iManager.getMovedAmount(); if (amount == null) { log.error("Corrupted dropping detected."); iManager.cancelDragging(); return; } if (ItemCount.isGreaterOne(amount) && isShiftPressed()) { numberSelect.requestNewPopup(1, amount.getValue(), new Callback() { @Override public void popupCanceled() { // nothing } @Override public void popupConfirmed(int value) { iManager.dropAtContainer(containerId, slotId, ItemCount.getInstance(value)); } }); } else { iManager.dropAtContainer(containerId, slotId, amount); } } /** * Check if the shift key is pressed on the keyboard. * * @return {@code true} in case the shift key on the keyboard */ private boolean isShiftPressed() { return input.isAnyKeyDown(Key.LeftShift, Key.RightShift); } @NiftyEventSubscriber(pattern = ".*container[0-9]+.*slot[0-9]+.*") public void onMouseMoveOverSlot(String topic, @Nonnull NiftyMouseMovedEvent event) { int slotId = getSlotId(topic); int containerId = getContainerId(topic); if (input.isAnyButtonDown(Button.Left, Button.Right)) { return; } ItemContainer container = World.getPlayer().getContainer(containerId); if (container == null) { log.error("MouseOver action on non-existent container!"); return; } ContainerSlot slot = container.getSlot(slotId); if (!LookAtTracker.isLookAtObject(slot)) { LookAtTracker.setLookAtObject(slot); slot.getInteractive().lookAt(); } } @Override public void bind(@Nonnull Nifty nifty, @Nonnull Screen screen) { activeNifty = nifty; activeScreen = screen; /* Lets build some new containers for the cache, so Merung is not crying that the container open to0 slowly. */ int preLoadBagCount = IllaClient.getCfg().getInteger("preLoadBagCount"); for (int i = 0; i < preLoadBagCount; i++) { itemContainerCache.add(buildNewContainer(100)); } } /** * Closes the given container * Hides the current ToolTip * @param containerId the ID of the container to close */ @Override public void closeContainer(int containerId) { World.getUpdateTaskManager().addTask((container, delta) -> { if (isContainerCreated(containerId)) { tooltipHandler.hideToolTip(); removeItemContainer(containerId); } }); } @Override public boolean isVisible(int containerId) { return isContainerCreated(containerId); } @Override public void onEndScreen() { if (activeNifty != null) { activeNifty.unsubscribeAnnotations(this); } Iterable<Integer> containerIds = new HashSet<>(itemContainerMap.keySet()); for (int id : containerIds) { removeItemContainer(id); } } @Override public void onStartScreen() { if (activeNifty != null) { activeNifty.subscribeAnnotations(this); } else { log.error("Initialization of ContainerHandler failed. No nifty instance. Container will not work."); } } @Override public void showContainer(@Nonnull ItemContainer container) { World.getUpdateTaskManager().addTask(new UpdateContainerTask(container)); } @Override public void showTooltip(int containerId, int slotId, @Nonnull Tooltip tooltip) { @Nullable org.illarion.nifty.controls.ItemContainer container = itemContainerMap.get(containerId); if (container == null) { return; } InventorySlot slot = container.getSlot(slotId); Element slotElement = slot.getElement(); if (slotElement != null) { Rectangle rect = new Rectangle(); rect.set(slotElement.getX(), slotElement.getY(), slotElement.getWidth(), slotElement.getHeight()); tooltipHandler.showToolTip(rect, tooltip); } } @Override public void updateMerchantOverlay() { World.getUpdateTaskManager().addTask(updateMerchantOverlays); } private int lastContainerId = -1; /** * Create a new container. * * @param itemContainer the item container the GUI is supposed to display */ private void createNewContainer(@Nonnull ItemContainer itemContainer) { /* First try to retrieve a existing container from the cache. */ org.illarion.nifty.controls.ItemContainer conControl = null; for (org.illarion.nifty.controls.ItemContainer cacheContainer : itemContainerCache) { if (cacheContainer.getSlotCount() == itemContainer.getSlotCount()) { conControl = cacheContainer; itemContainerCache.remove(cacheContainer); break; } } if (conControl == null) { conControl = buildNewContainer(itemContainer.getSlotCount()); } Element container = conControl.getElement(); conControl.setTitle(buildTitle(itemContainer)); String prefix = getPrefix(itemContainer.getContainerId()); container.setConstraintX(getSizeValueFromConfig(prefix + "DisplayPosX")); container.setConstraintY(getSizeValueFromConfig(prefix + "DisplayPosY")); if (!container.isVisible()) { container.show(); conControl.moveToFront(); } itemContainerMap.put(itemContainer.getContainerId(), conControl); } @Nonnull private org.illarion.nifty.controls.ItemContainer buildNewContainer(int slotCount) { String containerId = "container" + Integer.toString(++lastContainerId); ItemContainerBuilder builder = new ItemContainerBuilder(containerId, "NoTitle"); builder.slots(slotCount); builder.slotDim(35, 35); builder.hideOnClose(true); builder.visible(false); Element container = builder.build(activeNifty, activeScreen, activeScreen.findElementById("windows")); return container.getNiftyControl(org.illarion.nifty.controls.ItemContainer.class); } @Nonnull private static SizeValue getSizeValueFromConfig(@Nonnull String key) { String configEntry = IllaClient.getCfg().getString(key); if (configEntry == null) { return SizeValue.def(); } try { return new SizeValue(configEntry); } catch (IllegalArgumentException e) { if (configEntry.endsWith("px")) { try { float value = Float.parseFloat(configEntry.substring(0, configEntry.length() - 2)); return SizeValue.px((int) value); } catch (NumberFormatException e1) { // failed! } } return SizeValue.def(); } } @Nonnull @Contract(pure = true) private static String getPrefix(int containerId) { String prefix = "bag"; if (containerId == 0) { prefix = "backpack"; } else if (containerId < 10) { prefix = "depot"; } return prefix; } @Nonnull private static String buildTitle(@Nonnull ItemContainer container) { String title = container.getTitle(); String description = container.getDescription(); if (description.isEmpty()) { return title; } else { int slotsInRow = (int) Math.sqrt(container.getSlotCount()); Font calculationFont = FontLoader.getInstance().getFont(FontLoader.TEXT_FONT); int spaceToUse = (slotsInRow * 35) - 25; spaceToUse -= calculationFont.getWidth(title); String shortDescription = getShortenedDescription(description, "...", calculationFont, spaceToUse); if (shortDescription.isEmpty()) { return title; } return title + " (" + shortDescription + ')'; } } @Nonnull @Contract(pure = true) private static String getShortenedDescription( @Nonnull String description, @Nonnull String expansion, @Nonnull Font usedFont, int maxWidth) { if (maxWidth <= 0) { return ""; } if (usedFont.getWidth(description) <= maxWidth) { return description; } StringBuilder sb = new StringBuilder(description); while (sb.length() > 0) { sb.setLength(sb.length() - 1); sb.append(expansion); if (usedFont.getWidth(sb) <= maxWidth) { return sb.toString(); } sb.setLength(sb.length() - 3); } return ""; } /** * Update the merchant overlays of all active items. */ private void updateAllMerchantOverlays() { for (Entry<Integer, org.illarion.nifty.controls.ItemContainer> entry : itemContainerMap.entrySet()) { int id = entry.getKey(); org.illarion.nifty.controls.ItemContainer itemContainer = entry.getValue(); int slotCount = itemContainer.getSlotCount(); ItemContainer container = World.getPlayer().getContainer(id); if (container == null) { log.error("Container in handler was not created for player!"); continue; } for (int i = 0; i < slotCount; i++) { InventorySlot conSlot = itemContainer.getSlot(i); updateMerchantOverlay(conSlot, container.getSlot(i).getItemID()); } } } /** * Update the overlays of the merchants. * * @param slot the slot to update * @param itemId the item ID in this slot */ private void updateMerchantOverlay(@Nonnull InventorySlot slot, @Nullable ItemId itemId) { if (!ItemId.isValidItem(itemId)) { slot.hideMerchantOverlay(); return; } MerchantList merchantList = World.getPlayer().getMerchantList(); if (merchantList != null) { for (int i = 0; i < merchantList.getItemCount(); i++) { MerchantItem item = merchantList.getItem(i); if (item.getItemId().equals(itemId)) { switch (item.getType()) { case BuyingPrimaryItem: slot.showMerchantOverlay(MerchantBuyLevel.Gold); return; case BuyingSecondaryItem: slot.showMerchantOverlay(MerchantBuyLevel.Silver); return; case SellingItem: break; } } } } slot.hideMerchantOverlay(); } /** * Update the items inside the container. * * @param itemContainer the item container that contains the new data of the container */ private void updateContainer(@Nonnull ItemContainer itemContainer) { @Nullable org.illarion.nifty.controls.ItemContainer conControl = itemContainerMap .get(itemContainer.getContainerId()); if (conControl == null) { log.warn("Updating a container that does not exist."); return; } int slotCount = conControl.getSlotCount(); if (itemContainer.getSlotCount() != slotCount) { // something is badly wrong. The player class will handle this issue. return; } for (int i = 0; i < slotCount; i++) { ContainerSlot containerSlot = itemContainer.getSlot(i); InventorySlot conSlot = conControl.getSlot(i); ItemId itemId = containerSlot.getItemID(); ItemCount count = containerSlot.getCount(); if (ItemId.isValidItem(itemId) && ItemCount.isGreaterZero(count)) { ItemTemplate displayedItem = ItemFactory.getInstance().getTemplate(itemId.getValue()); NiftyImage niftyImage = new NiftyImage(activeNifty.getRenderEngine(), new EntitySlickRenderImage(displayedItem)); conSlot.setImage(niftyImage); conSlot.setLabelText(count.getShortText(Lang.getInstance().getLocale())); if (count.getValue() > 1) { conSlot.showLabel(); } else { conSlot.hideLabel(); } updateMerchantOverlay(conSlot, itemId); } else { conSlot.setImage(null); conSlot.hideLabel(); } } conControl.getElement().getParent().layoutElements(); } }