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.core.components;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.backup.BackupManager;
024import com.plotsquared.core.command.MainCommand;
025import com.plotsquared.core.configuration.caption.TranslatableCaption;
026import com.plotsquared.core.configuration.file.YamlConfiguration;
027import com.plotsquared.core.configuration.serialization.ConfigurationSerialization;
028import com.plotsquared.core.generator.ClassicPlotManagerComponent;
029import com.plotsquared.core.permissions.Permission;
030import com.plotsquared.core.player.PlotPlayer;
031import com.plotsquared.core.plot.Plot;
032import com.plotsquared.core.plot.PlotInventory;
033import com.plotsquared.core.plot.PlotItemStack;
034import com.plotsquared.core.queue.QueueCoordinator;
035import com.plotsquared.core.util.EconHandler;
036import com.plotsquared.core.util.InventoryUtil;
037import com.plotsquared.core.util.PatternUtil;
038import com.sk89q.worldedit.function.pattern.Pattern;
039import com.sk89q.worldedit.world.item.ItemTypes;
040import net.kyori.adventure.text.Component;
041import net.kyori.adventure.text.minimessage.MiniMessage;
042import net.kyori.adventure.text.minimessage.tag.Tag;
043import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
044import org.apache.logging.log4j.LogManager;
045import org.apache.logging.log4j.Logger;
046import org.checkerframework.checker.nullness.qual.NonNull;
047import org.checkerframework.checker.nullness.qual.Nullable;
048
049import java.io.File;
050import java.io.IOException;
051import java.nio.file.Files;
052import java.nio.file.Path;
053import java.nio.file.Paths;
054import java.util.ArrayList;
055import java.util.Collections;
056import java.util.List;
057import java.util.Map;
058import java.util.Objects;
059import java.util.stream.Collectors;
060
061public class ComponentPresetManager {
062
063    private static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build();
064    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + ComponentPresetManager.class.getSimpleName());
065
066    private final List<ComponentPreset> presets;
067    private final EconHandler econHandler;
068    private final InventoryUtil inventoryUtil;
069    private File componentsFile;
070
071    @SuppressWarnings("unchecked")
072    @Inject
073    public ComponentPresetManager(final @NonNull EconHandler econHandler, final @NonNull InventoryUtil inventoryUtil) throws
074            IOException {
075        this.econHandler = econHandler;
076        this.inventoryUtil = inventoryUtil;
077        final File oldLocation = new File(Objects.requireNonNull(PlotSquared.platform()).getDirectory(), "components.yml");
078        final File folder = new File(Objects.requireNonNull(PlotSquared.platform()).getDirectory(), "config");
079        if (!folder.exists() && !folder.mkdirs()) {
080            LOGGER.error("Failed to create the /plugins/PlotSquared/config folder. Please create it manually");
081        }
082        if (oldLocation.exists()) {
083            Path oldLoc = Paths.get(PlotSquared.platform().getDirectory() + "/components.yml");
084            Path newLoc = Paths.get(PlotSquared.platform().getDirectory() + "/config" + "/components.yml");
085            Files.move(oldLoc, newLoc);
086        }
087        try {
088            this.componentsFile = new File(folder, "components.yml");
089            if (!this.componentsFile.exists() && !this.componentsFile.createNewFile()) {
090                LOGGER.error("Could not create the components.yml file. Please create 'components.yml' manually.");
091            }
092        } catch (IOException e) {
093            e.printStackTrace();
094        }
095
096        ConfigurationSerialization.registerClass(ComponentPreset.class, "ComponentPreset");
097
098        final YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(this.componentsFile);
099
100        if (yamlConfiguration.contains("title")) {
101            yamlConfiguration.set("title", "#Now in /lang/messages_%.json, preset.title");
102            try {
103                yamlConfiguration.save(this.componentsFile);
104            } catch (IOException e) {
105                LOGGER.error("Failed to save default values to components.yml", e);
106            }
107        }
108
109        if (yamlConfiguration.contains("presets")) {
110            this.presets = yamlConfiguration
111                    .getMapList("presets")
112                    .stream()
113                    .map(o -> (Map<String, Object>) o)
114                    .map(ComponentPreset::deserialize)
115                    .collect(Collectors.toList());
116        } else {
117            final List<ComponentPreset> defaultPreset = Collections.singletonList(
118                    new ComponentPreset(
119                            ClassicPlotManagerComponent.FLOOR,
120                            "##wool",
121                            0,
122                            "",
123                            "<rainbow:2>Disco Floor</rainbow>",
124                            List.of("<gold>Spice up your plot floor</gold>"),
125                            ItemTypes.YELLOW_WOOL
126                    ));
127            yamlConfiguration.set("presets", defaultPreset.stream().map(ComponentPreset::serialize).collect(Collectors.toList()));
128            try {
129                yamlConfiguration.save(this.componentsFile);
130            } catch (final IOException e) {
131                LOGGER.error("Failed to save default values to components.yml", e);
132            }
133            this.presets = defaultPreset;
134        }
135
136        MainCommand.getInstance().register(new ComponentCommand(this));
137    }
138
139    /**
140     * Build the component inventory for a player. This also checks
141     * if the player is in a compatible plot, and sends appropriate
142     * error messages if not
143     *
144     * @param player player
145     * @return Build inventory, if it could be created
146     */
147    public @Nullable PlotInventory buildInventory(final PlotPlayer<?> player) {
148        final Plot plot = player.getCurrentPlot();
149
150        if (plot == null) {
151            player.sendMessage(TranslatableCaption.of("errors.not_in_plot"));
152            return null;
153        } else if (!plot.hasOwner()) {
154            player.sendMessage(TranslatableCaption.of("info.plot_unowned"));
155            return null;
156        } else if (!plot.isOwner(player.getUUID()) && !plot.getTrusted().contains(player.getUUID()) && !player.hasPermission(
157                Permission.PERMISSION_ADMIN_COMPONENTS_OTHER
158        )) {
159            player.sendMessage(TranslatableCaption.of("permission.no_plot_perms"));
160            return null;
161        } else if (plot.getVolume() > Integer.MAX_VALUE) {
162            player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large"));
163            return null;
164        }
165
166        final List<ComponentPreset> allowedPresets = new ArrayList<>(this.presets.size());
167        for (final ComponentPreset componentPreset : this.presets) {
168            if (!componentPreset.permission().isEmpty() && !player.hasPermission(
169                    componentPreset.permission()
170            )) {
171                continue;
172            }
173            allowedPresets.add(componentPreset);
174        }
175        if (allowedPresets.isEmpty()) {
176            player.sendMessage(TranslatableCaption.of("preset.empty"));
177            return null;
178        }
179        final int size = (int) Math.ceil((double) allowedPresets.size() / 9.0D);
180        final PlotInventory plotInventory = new PlotInventory(this.inventoryUtil, player, size,
181                TranslatableCaption.of("preset.title").getComponent(player)
182        ) {
183            @Override
184            public boolean onClick(final int index) {
185                if (!getPlayer().getCurrentPlot().equals(plot)) {
186                    return false;
187                }
188
189                if (index < 0 || index >= allowedPresets.size()) {
190                    return false;
191                }
192
193                final ComponentPreset componentPreset = allowedPresets.get(index);
194                if (componentPreset == null) {
195                    return false;
196                }
197
198                if (plot.getRunning() > 0) {
199                    getPlayer().sendMessage(TranslatableCaption.of("errors.wait_for_timer"));
200                    return false;
201                }
202
203                final Pattern pattern = PatternUtil.parse(null, componentPreset.pattern(), false);
204                if (pattern == null) {
205                    getPlayer().sendMessage(TranslatableCaption.of("preset.preset_invalid"));
206                    return false;
207                }
208
209                if (componentPreset.cost() > 0.0D && !player.hasPermission(Permission.PERMISSION_ADMIN_BYPASS_ECON)) {
210                    if (!econHandler.isEnabled(plot.getArea())) {
211                        getPlayer().sendMessage(
212                                TranslatableCaption.of("preset.economy_disabled"),
213                                TagResolver.resolver("preset", Tag.inserting(Component.text(componentPreset.displayName())))
214                        );
215                        return false;
216                    }
217                    if (econHandler.getMoney(getPlayer()) < componentPreset.cost()) {
218                        getPlayer().sendMessage(TranslatableCaption.of("preset.preset_cannot_afford"));
219                        return false;
220                    } else {
221                        econHandler.withdrawMoney(getPlayer(), componentPreset.cost());
222                        getPlayer().sendMessage(
223                                TranslatableCaption.of("economy.removed_balance"),
224                                TagResolver.resolver(
225                                        "money",
226                                        Tag.inserting(Component.text(econHandler.format(componentPreset.cost())))
227                                )
228                        );
229                    }
230                }
231
232                BackupManager.backup(getPlayer(), plot, () -> {
233                    plot.addRunning();
234                    QueueCoordinator queue = plot.getArea().getQueue();
235                    queue.setCompleteTask(plot::removeRunning);
236                    for (Plot current : plot.getConnectedPlots()) {
237                        current.getPlotModificationManager().setComponent(
238                                componentPreset.component().name(),
239                                pattern,
240                                player,
241                                queue
242                        );
243                    }
244                    queue.enqueue();
245                    getPlayer().sendMessage(TranslatableCaption.of("working.generating_component"));
246                });
247                return false;
248            }
249        };
250
251
252        for (int i = 0; i < allowedPresets.size(); i++) {
253            final ComponentPreset preset = allowedPresets.get(i);
254            final List<String> lore = new ArrayList<>();
255            if (preset.cost() > 0) {
256                if (!this.econHandler.isEnabled(plot.getArea())) {
257                    lore.add(MINI_MESSAGE.serialize(MINI_MESSAGE.deserialize(
258                            TranslatableCaption.of("preset.preset_lore_economy_disabled").getComponent(player))));
259                } else {
260                    lore.add(MINI_MESSAGE.serialize(MINI_MESSAGE.deserialize(
261                            TranslatableCaption.of("preset.preset_lore_cost").getComponent(player),
262                            TagResolver.resolver("cost", Tag.inserting(Component.text(String.format("%.2f", preset.cost()))))
263                    )));
264                }
265            }
266            lore.add(MINI_MESSAGE.serialize(MINI_MESSAGE.deserialize(
267                    TranslatableCaption.of("preset.preset_lore_component").getComponent(player),
268                    TagResolver.builder()
269                            .tag("component", Tag.inserting(Component.text(preset.component().name().toLowerCase())))
270                            .tag("prefix", Tag.inserting(TranslatableCaption.of("core.prefix").toComponent(player)))
271                            .build()
272            )));
273            lore.removeIf(String::isEmpty);
274            lore.addAll(preset.description());
275            plotInventory.setItem(
276                    i,
277                    new PlotItemStack(
278                            preset.icon().getId().replace("minecraft:", ""),
279                            1,
280                            preset.displayName(),
281                            lore.toArray(new String[0])
282                    )
283            );
284        }
285
286        return plotInventory;
287    }
288
289}