ch.njol.skript.bukkitutil.BukkitUnsafe.java Source code

Java tutorial

Introduction

Here is the source code for ch.njol.skript.bukkitutil.BukkitUnsafe.java

Source

/**
 *   This file is part of Skript.
 *
 *  Skript is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Skript 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Skript.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Copyright 2011-2017 Peter Gttinger and contributors
 */
package ch.njol.skript.bukkitutil;

import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.UnsafeValues;
import org.bukkit.inventory.ItemStack;
import org.eclipse.jdt.annotation.Nullable;

import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import ch.njol.skript.Skript;
import ch.njol.skript.util.Version;

/**
 * Contains helpers for Bukkit's not so safe stuff.
 */
@SuppressWarnings("deprecation")
public class BukkitUnsafe {

    /**
     * Bukkit's UnsafeValues allows us to do stuff that would otherwise
     * require NMS. It has existed for a long time, too, so 1.9 support is
     * not particularly hard to achieve.
     */
    private static final UnsafeValues unsafe;

    /**
     * 1.9 Spigot has some "fun" bugs.
     */
    private static final boolean knownNullPtr = !Skript.isRunningMinecraft(1, 11);

    static {
        UnsafeValues values = Bukkit.getUnsafe();
        if (values == null)
            throw new Error("UnsafeValues not available");
        unsafe = values;
    }

    /**
     * Before 1.13, Vanilla material names were translated using
     * this + a lookup table.
     */
    @Nullable
    private static MethodHandle unsafeFromInternalNameMethod;

    private static final boolean newMaterials = Skript.isRunningMinecraft(1, 13);

    /**
     * Vanilla material names to Bukkit materials.
     */
    @Nullable
    private static Map<String, Material> materialMap;

    /**
     * If we have material map for this version, using it is preferred.
     * Otherwise, it can be used as fallback.
     */
    private static boolean preferMaterialMap = true;

    /**
     * We only spit one exception (unless debugging) from UnsafeValues. Some
     * users might not care, and find 1.12 material mappings accurate enough.
     */
    private static boolean unsafeValuesErrored;

    /**
     * Maps pre 1.12 ids to materials for variable conversions.
     */
    @Nullable
    private static Map<Integer, Material> idMappings;

    public static void initialize() {
        if (!newMaterials) {
            MethodHandle mh;
            try {
                mh = MethodHandles.lookup().findVirtual(UnsafeValues.class, "getMaterialFromInternalName",
                        MethodType.methodType(Material.class, String.class));
            } catch (NoSuchMethodException | IllegalAccessException e) {
                mh = null;
            }
            unsafeFromInternalNameMethod = mh;

            try {
                Version version = Skript.getMinecraftVersion();
                boolean mapExists = loadMaterialMap(
                        "materials/" + version.getMajor() + "." + version.getMinor() + ".json");
                if (!mapExists) {
                    loadMaterialMap("materials/1.9.json"); // 1.9 is oldest we have mappings for
                    preferMaterialMap = false;
                    Skript.warning("Material mappings for " + version + " are not available.");
                    Skript.warning("Depending on your server software, some aliases may not work.");
                }
            } catch (IOException e) {
                Skript.exception(e, "Failed to load material mappings. Aliases may not work properly.");
            }
        }
    }

    @Nullable
    public static Material getMaterialFromMinecraftId(String id) {
        if (newMaterials) {
            // On 1.13, Vanilla and Spigot names are same
            if (id.length() > 9)
                return Material.matchMaterial(id.substring(10)); // Strip 'minecraft:' out
            else // Malformed material name
                return null;
        } else {
            // If we have correct material map, prefer using it
            if (preferMaterialMap) {
                if (id.length() > 9) {
                    assert materialMap != null;
                    return materialMap.get(id.substring(10)); // Strip 'minecraft:' out
                }
            }

            // Otherwise, hacks
            Material type = null;
            try {
                assert unsafeFromInternalNameMethod != null;
                type = (Material) unsafeFromInternalNameMethod.invokeExact(unsafe, id);
            } catch (Throwable e) {
                // Only spit out an error once unless debugging
                if (!unsafeValuesErrored || Skript.debug()) {
                    Skript.exception(e, "UnsafeValues failed to get material from Vanilla id");
                    unsafeValuesErrored = true;
                }
            }
            if (type == null || type == Material.AIR) { // If there is no item form, UnsafeValues won't work
                // So we're going to rely on 1.12's material mappings
                assert materialMap != null;
                return materialMap.get(id);
            }
            return type;
        }
    }

    private static boolean loadMaterialMap(String name) throws IOException {
        try (InputStream is = Skript.getInstance().getResource(name)) {
            if (is == null) { // No mappings for this Minecraft version
                return false;
            }
            String data = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);

            Type type = new TypeToken<Map<String, Material>>() {
            }.getType();
            materialMap = new Gson().fromJson(data, type);
        }

        return true;
    }

    public static void modifyItemStack(ItemStack stack, String arguments) {
        try {
            unsafe.modifyItemStack(stack, arguments);
        } catch (NullPointerException e) {
            if (knownNullPtr) { // Probably known Spigot bug
                // So we continue doing whatever we were doing and hope it works
                Skript.warning("Item " + stack.getType() + arguments
                        + " failed modifyItemStack. This is a bug on old Spigot versions.");
            } else { // Not known null pointer, don't just swallow
                throw e;
            }
        }
    }

    private static void initIdMappings() {
        try (InputStream is = Skript.getInstance().getResource("materials/ids.json")) {
            if (is == null) {
                throw new AssertionError("missing id mappings");
            }
            String data = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);

            Type type = new TypeToken<Map<Integer, String>>() {
            }.getType();
            Map<Integer, String> rawMappings = new Gson().fromJson(data, type);

            // Process raw mappings
            Map<Integer, Material> parsed = new HashMap<>(rawMappings.size());
            if (newMaterials) { // Legacy material conversion API
                for (Map.Entry<Integer, String> entry : rawMappings.entrySet()) {
                    parsed.put(entry.getKey(), Material.matchMaterial(entry.getValue(), true));
                }
            } else { // Just enum API
                for (Map.Entry<Integer, String> entry : rawMappings.entrySet()) {
                    parsed.put(entry.getKey(), Material.valueOf(entry.getValue()));
                }
            }
            idMappings = parsed;
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }

    @Nullable
    public static Material getMaterialFromId(int id) {
        if (idMappings == null) {
            initIdMappings();
        }
        assert idMappings != null;
        return idMappings.get(id);
    }
}