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.command;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.configuration.ConfigurationNode;
024import com.plotsquared.core.configuration.ConfigurationSection;
025import com.plotsquared.core.configuration.ConfigurationUtil;
026import com.plotsquared.core.configuration.InvalidConfigurationException;
027import com.plotsquared.core.configuration.Settings;
028import com.plotsquared.core.configuration.caption.TranslatableCaption;
029import com.plotsquared.core.configuration.file.YamlConfiguration;
030import com.plotsquared.core.events.TeleportCause;
031import com.plotsquared.core.inject.annotations.WorldConfig;
032import com.plotsquared.core.inject.annotations.WorldFile;
033import com.plotsquared.core.permissions.Permission;
034import com.plotsquared.core.player.PlotPlayer;
035import com.plotsquared.core.plot.PlotArea;
036import com.plotsquared.core.plot.PlotManager;
037import com.plotsquared.core.plot.world.PlotAreaManager;
038import com.plotsquared.core.setup.PlotAreaBuilder;
039import com.plotsquared.core.setup.SettingsNodesWrapper;
040import com.plotsquared.core.util.FileBytes;
041import com.plotsquared.core.util.FileUtils;
042import com.plotsquared.core.util.SetupUtils;
043import com.plotsquared.core.util.TabCompletions;
044import com.plotsquared.core.util.WorldUtil;
045import com.plotsquared.core.util.task.TaskManager;
046import net.kyori.adventure.text.Component;
047import net.kyori.adventure.text.minimessage.tag.Tag;
048import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
049import org.checkerframework.checker.nullness.qual.NonNull;
050
051import java.io.File;
052import java.io.FileInputStream;
053import java.io.FileOutputStream;
054import java.io.IOException;
055import java.util.Collection;
056import java.util.Collections;
057import java.util.LinkedList;
058import java.util.List;
059import java.util.Set;
060import java.util.stream.Collectors;
061import java.util.zip.ZipEntry;
062import java.util.zip.ZipInputStream;
063import java.util.zip.ZipOutputStream;
064
065@CommandDeclaration(command = "template",
066        permission = "plots.admin",
067        usage = "/plot template [import | export] <world> <template>",
068        category = CommandCategory.ADMINISTRATION)
069public class Template extends SubCommand {
070
071    private final PlotAreaManager plotAreaManager;
072    private final YamlConfiguration worldConfiguration;
073    private final File worldFile;
074    private final SetupUtils setupUtils;
075    private final WorldUtil worldUtil;
076
077    @Inject
078    public Template(
079            final @NonNull PlotAreaManager plotAreaManager,
080            @WorldConfig final @NonNull YamlConfiguration worldConfiguration,
081            @WorldFile final @NonNull File worldFile,
082            final @NonNull SetupUtils setupUtils,
083            final @NonNull WorldUtil worldUtil
084    ) {
085        this.plotAreaManager = plotAreaManager;
086        this.worldConfiguration = worldConfiguration;
087        this.worldFile = worldFile;
088        this.setupUtils = setupUtils;
089        this.worldUtil = worldUtil;
090    }
091
092    public static boolean extractAllFiles(String world, String template) {
093        try {
094            File folder =
095                    FileUtils.getFile(PlotSquared.platform().getDirectory(), Settings.Paths.TEMPLATES);
096            if (!folder.exists()) {
097                return false;
098            }
099            File output = PlotSquared.platform().getDirectory();
100            if (!output.exists()) {
101                output.mkdirs();
102            }
103            File input = new File(folder + File.separator + template + ".template");
104            try (ZipInputStream zis = new ZipInputStream(new FileInputStream(input))) {
105                ZipEntry ze = zis.getNextEntry();
106                byte[] buffer = new byte[2048];
107                while (ze != null) {
108                    if (!ze.isDirectory()) {
109                        String name = ze.getName().replace('\\', File.separatorChar)
110                                .replace('/', File.separatorChar);
111                        File newFile = new File(
112                                (output + File.separator + name).replaceAll("__TEMP_DIR__", world));
113                        File parent = newFile.getParentFile();
114                        if (parent != null) {
115                            parent.mkdirs();
116                        }
117                        try (FileOutputStream fos = new FileOutputStream(newFile)) {
118                            int len;
119                            while ((len = zis.read(buffer)) > 0) {
120                                fos.write(buffer, 0, len);
121                            }
122                        }
123                    }
124                    ze = zis.getNextEntry();
125                }
126                zis.closeEntry();
127            }
128            return true;
129        } catch (IOException e) {
130            e.printStackTrace();
131            return false;
132        }
133    }
134
135    public static byte[] getBytes(PlotArea plotArea) {
136        ConfigurationSection section = PlotSquared
137                .get()
138                .getWorldConfiguration()
139                .getConfigurationSection("worlds." + plotArea.getWorldName());
140        YamlConfiguration config = new YamlConfiguration();
141        String generator = PlotSquared.platform().setupUtils().getGenerator(plotArea);
142        if (generator != null) {
143            config.set("generator.plugin", generator);
144        }
145        for (String key : section.getKeys(true)) {
146            config.set(key, section.get(key));
147        }
148        return config.saveToString().getBytes();
149    }
150
151    public static void zipAll(String world, Set<FileBytes> files) throws IOException {
152        File output = FileUtils.getFile(PlotSquared.platform().getDirectory(), Settings.Paths.TEMPLATES);
153        output.mkdirs();
154        try (FileOutputStream fos = new FileOutputStream(
155                output + File.separator + world + ".template");
156             ZipOutputStream zos = new ZipOutputStream(fos)) {
157
158            for (FileBytes file : files) {
159                ZipEntry ze = new ZipEntry(file.path());
160                zos.putNextEntry(ze);
161                zos.write(file.data());
162            }
163            zos.closeEntry();
164        }
165    }
166
167    @Override
168    public boolean onCommand(final PlotPlayer<?> player, String[] args) {
169        if (args.length != 2 && args.length != 3) {
170            if (args.length == 1) {
171                if (args[0].equalsIgnoreCase("export")) {
172                    player.sendMessage(
173                            TranslatableCaption.of("commandconfig.command_syntax"),
174                            TagResolver.resolver("value", Tag.inserting(Component.text("/plot template export <world>")))
175                    );
176                    return true;
177                } else if (args[0].equalsIgnoreCase("import")) {
178                    player.sendMessage(
179                            TranslatableCaption.of("commandconfig.command_syntax"),
180                            TagResolver.resolver(
181                                    "value",
182                                    Tag.inserting(Component.text("/plot template import <world> <template>"))
183                            )
184                    );
185                    return true;
186                }
187            }
188            sendUsage(player);
189            return true;
190        }
191        final String world = args[1];
192        switch (args[0].toLowerCase()) {
193            case "import" -> {
194                if (args.length != 3) {
195                    player.sendMessage(
196                            TranslatableCaption.of("commandconfig.command_syntax"),
197                            TagResolver.resolver(
198                                    "value",
199                                    Tag.inserting(Component.text("/plot template import <world> <template>"))
200                            )
201                    );
202                    return false;
203                }
204                if (this.plotAreaManager.hasPlotArea(world)) {
205                    player.sendMessage(
206                            TranslatableCaption.of("setup.setup_world_taken"),
207                            TagResolver.resolver("value", Tag.inserting(Component.text(world)))
208                    );
209                    return false;
210                }
211                boolean result = extractAllFiles(world, args[2]);
212                if (!result) {
213                    player.sendMessage(
214                            TranslatableCaption.of("template.invalid_template"),
215                            TagResolver.resolver("value", Tag.inserting(Component.text(args[2])))
216                    );
217                    return false;
218                }
219                File worldFile = FileUtils.getFile(
220                        PlotSquared.platform().getDirectory(),
221                        Settings.Paths.TEMPLATES + File.separator + "tmp-data.yml"
222                );
223                YamlConfiguration worldConfig = YamlConfiguration.loadConfiguration(worldFile);
224                this.worldConfiguration.set("worlds." + world, worldConfig.get(""));
225                try {
226                    this.worldConfiguration.save(this.worldFile);
227                    this.worldConfiguration.load(this.worldFile);
228                } catch (InvalidConfigurationException | IOException e) {
229                    e.printStackTrace();
230                }
231                String manager =
232                        worldConfig.getString("generator.plugin", PlotSquared.platform().pluginName());
233                String generator = worldConfig.getString("generator.init", manager);
234                PlotAreaBuilder builder = PlotAreaBuilder.newBuilder()
235                        .plotAreaType(ConfigurationUtil.getType(worldConfig))
236                        .terrainType(ConfigurationUtil.getTerrain(worldConfig))
237                        .plotManager(manager)
238                        .generatorName(generator)
239                        .settingsNodesWrapper(new SettingsNodesWrapper(new ConfigurationNode[0], null))
240                        .worldName(world);
241
242                this.setupUtils.setupWorld(builder);
243                TaskManager.runTask(() -> {
244                    player.teleport(this.worldUtil.getSpawn(world), TeleportCause.COMMAND_TEMPLATE);
245                    player.sendMessage(TranslatableCaption.of("setup.setup_finished"));
246                });
247                return true;
248            }
249            case "export" -> {
250                if (args.length != 2) {
251                    player.sendMessage(
252                            TranslatableCaption.of("commandconfig.command_syntax"),
253                            TagResolver.resolver("value", Tag.inserting(Component.text("/plot template export <world>")))
254                    );
255                    return false;
256                }
257                final PlotArea area = this.plotAreaManager.getPlotAreaByString(world);
258                if (area == null) {
259                    player.sendMessage(
260                            TranslatableCaption.of("errors.not_valid_plot_world"),
261                            TagResolver.resolver("value", Tag.inserting(Component.text(args[1])))
262                    );
263                    return false;
264                }
265                final PlotManager manager = area.getPlotManager();
266                TaskManager.runTaskAsync(() -> {
267                    try {
268                        manager.exportTemplate();
269                    } catch (Exception e) { // Must recover from any exception thrown a third party template manager
270                        e.printStackTrace();
271                        player.sendMessage(
272                                TranslatableCaption.of("template.template_failed"),
273                                TagResolver.resolver("value", Tag.inserting(Component.text(e.getMessage())))
274                        );
275                        return;
276                    }
277                    player.sendMessage(TranslatableCaption.of("setup.setup_finished"));
278                });
279                return true;
280            }
281            default -> sendUsage(player);
282        }
283        return false;
284    }
285
286    @Override
287    public Collection<Command> tab(final PlotPlayer<?> player, final String[] args, final boolean space) {
288        if (args.length == 1) {
289            final List<String> completions = new LinkedList<>();
290            if (player.hasPermission(Permission.PERMISSION_TEMPLATE_EXPORT)) {
291                completions.add("export");
292            }
293            if (player.hasPermission(Permission.PERMISSION_TEMPLATE_IMPORT)) {
294                completions.add("import");
295            }
296            final List<Command> commands = completions.stream().filter(completion -> completion
297                            .toLowerCase()
298                            .startsWith(args[0].toLowerCase()))
299                    .map(completion -> new Command(
300                            null,
301                            true,
302                            completion,
303                            "",
304                            RequiredType.NONE,
305                            CommandCategory.ADMINISTRATION
306                    ) {
307                    }).collect(Collectors.toCollection(LinkedList::new));
308            if (player.hasPermission(Permission.PERMISSION_TEMPLATE) && args[0].length() > 0) {
309                commands.addAll(TabCompletions.completePlayers(player, args[0], Collections.emptyList()));
310            }
311            return commands;
312        }
313        return TabCompletions.completePlayers(player, String.join(",", args).trim(), Collections.emptyList());
314    }
315
316}