001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.bukkit.util;
020
021import com.google.common.collect.Maps;
022import com.plotsquared.core.location.World;
023import net.kyori.adventure.key.Key;
024import org.bukkit.Bukkit;
025import org.bukkit.NamespacedKey;
026import org.checkerframework.checker.nullness.qual.NonNull;
027import org.jetbrains.annotations.NotNull;
028
029import java.lang.ref.SoftReference;
030import java.lang.ref.WeakReference;
031import java.nio.file.Path;
032import java.util.Map;
033
034public class BukkitWorld implements World<org.bukkit.World> {
035
036    // Upon world unload we should probably have the P2 BukkitWorld be GCed relatively eagerly, thus freeing the bukkit world.
037    //  We also want to avoid circumstances where a bukkit world has been GCed, but a P2 BukkitWorld has not
038    private static final Map<String, WeakReference<BukkitWorld>> worldMap = Maps.newHashMap();
039    private static final boolean HAS_MIN_Y;
040
041    static {
042        boolean temp;
043        try {
044            org.bukkit.World.class.getMethod("getMinHeight");
045            temp = true;
046        } catch (NoSuchMethodException e) {
047            temp = false;
048        }
049        HAS_MIN_Y = temp;
050    }
051
052    // We want to allow GC to remove bukkit worlds, but not too eagerly
053    private final SoftReference<org.bukkit.World> world;
054    private Key key = null;
055
056    private BukkitWorld(final org.bukkit.World world) {
057        this.world = new SoftReference<>(world);
058    }
059
060    /**
061     * Get a new {@link BukkitWorld} from a world name
062     *
063     * @param worldName World name
064     * @return World instance
065     */
066    public static @NonNull BukkitWorld of(final @NonNull String worldName) {
067        final org.bukkit.World bukkitWorld = Bukkit.getWorld(worldName);
068        if (bukkitWorld == null) {
069            throw new IllegalArgumentException(String.format("There is no world with the name '%s'", worldName));
070        }
071        return of(bukkitWorld);
072    }
073
074    /**
075     * Get a new {@link BukkitWorld} from a Bukkit world
076     *
077     * @param world Bukkit world
078     * @return World instance
079     */
080    public static @NonNull BukkitWorld of(final org.bukkit.World world) {
081        WeakReference<BukkitWorld> bukkitWorldRef = worldMap.get(world.getName());
082        BukkitWorld bukkitWorld;
083        if (bukkitWorldRef != null && (bukkitWorld = bukkitWorldRef.get()) != null && world.equals(bukkitWorld.world.get())) {
084            return bukkitWorld;
085        }
086        bukkitWorld = new BukkitWorld(world);
087        worldMap.put(world.getName(), new WeakReference<>(bukkitWorld));
088        return bukkitWorld;
089    }
090
091    /**
092     * Get the min world height from a Bukkit {@link org.bukkit.World}. Inclusive
093     *
094     * @since 6.6.0
095     */
096    public static int getMinWorldHeight(org.bukkit.World world) {
097        return HAS_MIN_Y ? world.getMinHeight() : 0;
098    }
099
100    /**
101     * Get the max world height from a Bukkit {@link org.bukkit.World}. Exclusive
102     *
103     * @since 6.6.0
104     */
105    public static int getMaxWorldHeight(org.bukkit.World world) {
106        return HAS_MIN_Y ? world.getMaxHeight() : 256;
107    }
108
109    @Override
110    public org.bukkit.@NonNull World getPlatformWorld() {
111        org.bukkit.World world = this.world.get();
112        if (world == null) {
113            throw new IllegalStateException("Bukkit platform world was unloaded from memory");
114        }
115        return world;
116    }
117
118    @Override
119    public @NonNull String getName() {
120        return this.getPlatformWorld().getName();
121    }
122
123    @Override
124    public int getMinHeight() {
125        return getMinWorldHeight(getPlatformWorld());
126    }
127
128    @Override
129    public int getMaxHeight() {
130        return getMaxWorldHeight(getPlatformWorld()) - 1;
131    }
132
133    @Override
134    public @NonNull Path getWorldFolder() {
135        return getPlatformWorld().getWorldFolder().toPath();
136    }
137
138    @Override
139    @SuppressWarnings("PatternValidation") // kyori pattern validation...
140    public @NotNull Key key() {
141        if (this.key == null) {
142            NamespacedKey bukkitKey = getPlatformWorld().getKey();
143            this.key = Key.key(bukkitKey.getNamespace(), bukkitKey.getKey());
144        }
145        return this.key;
146    }
147
148    @Override
149    public boolean equals(final Object o) {
150        if (this == o) {
151            return true;
152        }
153        if (o == null || getClass() != o.getClass()) {
154            return false;
155        }
156        final BukkitWorld that = (BukkitWorld) o;
157        return world.equals(that.world);
158    }
159
160    @Override
161    public int hashCode() {
162        return world.hashCode();
163    }
164
165    public String toString() {
166        return "BukkitWorld(world=" + this.world + ")";
167    }
168
169}