Skip to content

Customising spectator inventories

Jan edited this page Apr 11, 2023 · 7 revisions

SpectatorInventory Customisations

Customisations to the SpectatorInventories can be done via the CreationOptions class. You can create an instance of this class by using the static factory methods CreationOptions#of(...), CreationOptions#defaultMainInventory(Plugin) or CreationOptions#defaultEnderInventory(Plugin). If you want to use the values for these options configured in InvSee++'s own config, use one of the following methods:

  • InvseeAPI#mainInventoryCreationOptions(Player spectator) --> will respect bypass exempt permission
  • InvseeAPI#enderInventoryCreationOptions(Player spectator) --> will respect bypass exempt permission
  • InvseeAPI#mainInventoryCreationOptions() --> will not be able to bypass target's exemption status
  • InvseeAPI#enderInventoryCreationOptions() --> will not be able to bypass target's exemption status

These four methods always return new instances.

Then, use one of the following methods to customise the inventory creation process:

  • withTitle(Title) to customise the title of the opened inventory.
  • withOfflinePlayerSupport(boolean) to customise whether InvSee++ will create the SpectatorInventory in case the target player is offline.
  • withUnknownPlayerSupport(boolean) to customise whether InvSee++ will create the SpectatorInventory in case the target has not been on the server before.
  • withBypassExemptedPlayers(boolean) to customise whether the spectator can bypass the target's exemption status.
  • withLogOptions(LogOptions) to customise how interactions with the created SpectatorInventory should be logged.
  • withMirror(Mirror) to customise the view of the spectating player.

These 'withers' always return the same CreationOptions again, so you can chain method calls to create your desired CreationOptions. The options themselves will be explained below.

The CreationOptions can be used by passing them to one of the following InvseeAPI methods:

  • #mainSpectatorInventory(HumanEntity target, CreationOptions<PlayerInventorySlot> options)
  • #mainSpectatorInventory(String targetName, CreationOptions<PlayerInventorySlot> options)
  • #mainSpectatorInventory(UUID targetId, String targetName, CreationOptions<PlayerInventorySlot> options)
  • #spectateInventory(Player spectator, String targetName, CreationOptions<PlayerInventorySlot> options)
  • #spectateInventory(Player spectator, UUId targetId, String targetName, CreationOptions<PlayerInventorySlot> options)

..and similar for spectating the ender chest: #enderSpectatorInventory and #spectateEnderChest.

CreationOption parameters

Title

The withTitle method is used the customise the title of the to-be-created SpectatorInventory. Title is a function from Target to String, meaning you could implement it using a lambda expression. It's also possible to provide a constant Title using the factory method Title#of(String).

Offline player support

The withOfflinePlayerSupport method customises whether the InvseeAPI will attempt to create a SpectatorInventory for a target player who is offline. When true is provided then InvSee++ will always try to create a SpectatorInventory, regardless of whether the player is online. This means you will never get a SpectateResponse that fails with reason OfflineSupportDisabled.

Similarly, when offlineSupport is false, then the InvseeAPI will not even bother trying to create an offline spectator inventory - you will only get a SpectateResponse that succeeds if the target player is online.

Unknown player support

Similar to offline player support, this option allows you to configure whether the InvseeAPI will attempt to create a SpectatorInventory if the target player has not logged on to the server before. Pass true if you want to allow SpectatorInventories for players who have not been on the server before. Pass false to disallow the creation of a SpectatorInventory if the player has not been on the server before.

Bypass exempted players

When this option is set to true, the InvseeAPI will ignore whether the target player has the invseeplusplus.exempt.invsee or invseeplusplus.exempt.endersee permission. When false is provided, the InvseeAPI will respect these exemption permissions.

You can check whether a player has the bypass-exempt permission using Bukkit's permission api: player.hasPermission(Exempt.BYPASS_EXEMPT_INVENTORY) or player.hasPermission(Exempt.BYPASS_EXEMPT_ENDERCHEST)

Logging options

LogOptions is the class for configuring the logging options. New instances can be created using LogOptions#of(LogGranularity, Set<LogTarget>, EnumMap<LogTarget, String>). The parameters will be explained below:

LogGranularity - when should changes to the SpectatorInventory be logged?

  • Use LOG_NEVER to not log any changes.
  • Use LOG_ON_CLOSE to log the changes to the spectator inventory when spectating player closes the inventory.
  • Use LOG_EVERY_CHANGE to log the changes after every inventory click.

LogTarget - where should changes to the SpectatorInventory be logged?

  • Use LogTarget.CONSOLE to only log to the console.
  • Use LogTarget.SERVER_LOG_FILE to log to the latest.log. If this option is used, the changes will also be logged to the console.
  • Use LogTarget.PLUGIN_LOG_FILE to log changes to _global.log file in InvSee++'s data folder.
  • Use LogTarget.SPECTATOR_LOG_FILE to log changes to the <uuid>.log file in InvSee++'s data folder.

You can provide a java.util.Set in order to mix and match LogTargets.

Log formats

You can set the format strings that will be used by the loggers, separate for each LogTarget. There are some special macros that will be replaced:

  • <spectator_uuid> to log the UUID of the spectating player.
  • <spectator_name> to log the username of the spectating player.
  • <taken> to log the items taken from the SpectatorInventory.
  • <given> to log the items given to the SpectatorInventory. ` to log the target player of the SpectatorInventory.

There is also LogOptions#empty() to obtain LogOptions that never log any changes.

Mirror

Mirrors enable you to change the order of the slots in which admin players see the items. Let us draw a schematic first:

                              (target inventory)
                                      |
                                      |
                                      |
                    <spectates>       V                
   (admin player) ---------------> [Mirror]
          (image) <---------------
                    <reflects>

The Mirror warps the view of the admin player, while the real target inventory keeps its original item order. Mirror is defined as an interface with just 2 methods:

  • getIndex(slot) - returns the index that the slot is mapped to.
  • getSlot(index) - return the slot at the position of that index. Both these methods are the duals of one-another, i.e. java.util.Objects.equals(slot, getSlot(getIndex(slot))) and java.util.Objects.equals(index, getIndex(getSlot(index))).

It is possible to obtain the default indices of the built-in slot types with:

  • PlayerInventorySlot#defaultIndex() -> int
  • EnderChestSlot#defaultIndex() -> int And the reverse is also possible:
  • PlayerInventorySlot#byDefaultIndex(int) -> PlayerInventorySlot
  • EnderChestSlot#byDefaultIndex(int) -> EnderChestSlot

The Slot type is parameterised - the concrete mirror implementation differs per SpectatorInventory type. MainSpectatorInventory uses slot type PlayerInventorySlot, whereas EnderSpectatorInventory uses EnderChestSlot.

By default, the order of items of a MainSpectatorInventory is as follows:

  • 1st row (slots 0 through 8): Display the target's hotbar items (index 0 through 8).
  • 2nd, 3rd and 4th row (slots 9 through 35): Display the target's other items in the 'storage' part of their inventory (index 9 through 35).
  • 5th row (slots 36 through 41): Display the target's boots (slot 36), leggings (slot 37), chestplate (slot 38), helmet (slot 39), off-hand (slot 40) and curstor (slot 41).
  • 6th row: Display the target's 'personal' items - the items that are in their crafting window, enchanting window, anvil, or any of the villager working stations. The number of slots used for this purpose can vary between 4 and 9 (slots 45 through 49/54).

But using a Mirror, we can change this perspective completely! Let us implement a Mirror that reverses the first four rows:

    private final Mirror<PlayerInventorySlot> reverse1st4Rows = new Mirror<PlayerInventorySlot>() {
        @Override
        public Integer getIndex(PlayerInventorySlot slot) {
            int defaultIndex = slot.defaultIndex();
            if (0 <= defaultIndex && defaultIndex < 9) {
                return defaultIndex + 27;
            } else if (9 <= defaultIndex && defaultIndex < 18) {
                return defaultIndex + 9;
            } else if (18 <= defaultIndex && defaultIndex < 27) {
                return defaultIndex - 9;
            } else if (27 <= defaultIndex && defaultIndex < 36) {
                return defaultIndex - 27;
            } else {
                return defaultIndex;
            }
        }

        @Override
        public PlayerInventorySlot getSlot(int index) { //index is the 'warped' index.
            if (0 <= index && index < 9) {
                return PlayerInventorySlot.byDefaultIndex(index + 27);
            } else if (9 <= index && index < 18) {
                return PlayerInventorySlot.byDefaultIndex(index + 9);
            } else if (18 <= index && index < 27) {
                return PlayerInventorySlot.byDefaultIndex(index - 9);
            } else if (27 <= index && index < 36) {
                return PlayerInventorySlot.byDefaultIndex(index - 27);
            } else if (36 <= index && index < 54) {
                return PlayerInventorySlot.byDefaultIndex(index);
            } else {
                return null;
            }
        }
    };

When we use this mirror to create a MainSpectatorInventory, we get the following result: image

We also note that EnderChestSlot has 54 enum constants, that is because the Purpur server implemenation supports ender chests up to size 54! On regular CraftBukkit/Spigot/Paper, only the first 27 slots are used.

Imporatant Note that Mirrors only change the perspective of admin players - they DO NOT change the order of slots of the SpectatorInventory. This means it is completely safe to assume that mainSpectatorInventory.getItem(PlayerInventorySlot.ARMOUR_BOOTS.defaultIndex()) will always return the ItemStack at the boots armour slot!

Going one step further.

We've now learned how we can customise the default properties of spectator inventories, but it is also possible to customise those properties on a per-window basis! In other words, it is possible for two different admin players to spectate the same target player simultaniously, both using different inventory titles and mirrors! To achieve this, use the Api methods:

  • InvseeAPI#spectateInventory(Player spectator, String targetName, CreationOptions<PlayerInventorySlot> options)
  • InvseeAPI#spectateEnderChest(Player spectator, String targetName, CreationOptions<EnderChestSlot> options) Both of these methods return a CompletableFuture, meaning that the Api deals with NotCreatedReasons internally.