Просмотр исходного кода

Initial commit

Signed-off-by: Daniel Nägele <info@naegele.dev>
Daniel Nägele 3 лет назад
Сommit
21f939defa
100 измененных файлов с 10388 добавлено и 0 удалено
  1. 24 0
      .gitignore
  2. 40 0
      1_12/pom.xml
  3. 61 0
      1_12/src/main/java/de/butzlabben/missilewars/missile/paste/r1_12/BlockFilterExtent.java
  4. 117 0
      1_12/src/main/java/de/butzlabben/missilewars/missile/paste/r1_12/R1_12Paster.java
  5. 55 0
      1_13/pom.xml
  6. 51 0
      1_13/src/main/java/de/butzlabben/missilewars/missile/paste/r1_13/we/BlockFilterExtent.java
  7. 118 0
      1_13/src/main/java/de/butzlabben/missilewars/missile/paste/r1_13/we/R1_13Paster.java
  8. BIN
      1_13_FAWE/lib/FastAsyncWorldEdit.jar
  9. 43 0
      1_13_FAWE/pom.xml
  10. 97 0
      1_13_FAWE/src/main/java/de/butzlabben/missilewars/missile/paste/r1_13/fawe/R1_13Paster.java
  11. 43 0
      1_16_FAWE/pom.xml
  12. 93 0
      1_16_FAWE/src/main/java/de/butzlabben/missilewars/missile/paste/r1_16/fawe/R1_16Paster.java
  13. 675 0
      LICENSE
  14. 124 0
      missilewars-plugin/pom.xml
  15. 293 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/Config.java
  16. 51 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/Logger.java
  17. 134 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/MessageConfig.java
  18. 182 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/MissileWars.java
  19. 226 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/cmd/MWCommands.java
  20. 217 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/cmd/StatsCommands.java
  21. 164 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/cmd/UserCommands.java
  22. 80 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Arenas.java
  23. 466 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Game.java
  24. 144 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameManager.java
  25. 30 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameState.java
  26. 44 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/EndTimer.java
  27. 76 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/GameTimer.java
  28. 142 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/LobbyTimer.java
  29. 59 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/Timer.java
  30. 34 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/CustomInv.java
  31. 28 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/DependListener.java
  32. 27 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcClickListener.java
  33. 167 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcInventory.java
  34. 150 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcItem.java
  35. 71 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcListener.java
  36. 77 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/VoteInventory.java
  37. 71 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/pages/InventoryPage.java
  38. 32 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/pages/ItemConverter.java
  39. 101 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/pages/PageGUICreator.java
  40. 94 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/EndListener.java
  41. 44 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/GameBoundListener.java
  42. 335 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/GameListener.java
  43. 153 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/LobbyListener.java
  44. 226 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/PlayerListener.java
  45. 54 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/signs/ClickListener.java
  46. 85 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/signs/ManageListener.java
  47. 120 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/ConnectionHolder.java
  48. 48 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/MathUtil.java
  49. 73 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/MoneyUtil.java
  50. 61 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/MotdManager.java
  51. 86 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/PlayerDataProvider.java
  52. 114 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/Randomizer.java
  53. 76 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/ScoreboardManager.java
  54. 217 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/SetupUtil.java
  55. 93 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/serialization/LocationTypeAdapter.java
  56. 147 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/serialization/Serializer.java
  57. 177 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/GameProfileBuilder.java
  58. 141 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/PlayerGuiFactory.java
  59. 131 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/PreFetcher.java
  60. 38 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/StatsUtil.java
  61. 32 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/BlockDataSetter.java
  62. 114 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/BlockSetterProvider.java
  63. 244 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/ColorConverter.java
  64. 270 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/VersionUtil.java
  65. 102 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/Arena.java
  66. 139 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/GameWorld.java
  67. 92 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/Lobby.java
  68. 23 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/MapChooseProcedure.java
  69. 33 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/FallProtectionConfiguration.java
  70. 34 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/FireballConfiguration.java
  71. 102 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/MissileConfiguration.java
  72. 33 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/MoneyConfiguration.java
  73. 35 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/ShieldConfiguration.java
  74. 46 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/GameEndEvent.java
  75. 31 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/GameEvent.java
  76. 40 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/GameStartEvent.java
  77. 49 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/PlayerArenaJoinEvent.java
  78. 49 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/PlayerArenaLeaveEvent.java
  79. 61 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/PrePlayerArenaJoinEvent.java
  80. 139 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/game/RespawnGoldBlock.java
  81. 93 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/game/Shield.java
  82. 201 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/game/Team.java
  83. 112 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/Area.java
  84. 40 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/FlatArea.java
  85. 71 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/Line.java
  86. 69 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/Plane.java
  87. 119 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/Missile.java
  88. 125 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/MissileFacing.java
  89. 60 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/PasteProvider.java
  90. 34 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/Paster.java
  91. 44 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_12PasteProvider.java
  92. 43 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_13FawePasteProvider.java
  93. 44 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_13WEPasteProvider.java
  94. 46 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_16FawePasteProvider.java
  95. 64 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/player/Interval.java
  96. 96 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/player/MWPlayer.java
  97. 104 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/player/PlayerData.java
  98. 31 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/signs/CheckRunnable.java
  99. 111 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/signs/MWSign.java
  100. 93 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/signs/SignRepository.java

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+
+# Eclipse
+bin/
+.settings
+.project
+.classpath
+
+# IntelliJ
+out/
+.idea/
+*.iml
+
+# Misc
+.DS_Store

+ 40 - 0
1_12/pom.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+  ~ Copyright (c) 2018-2021 Daniel Nägele.
+  ~
+  ~ MissileWars 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.
+  ~
+  ~ MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>missilewars</artifactId>
+        <groupId>de.butzlabben</groupId>
+        <version>1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>1_12</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.sk89q</groupId>
+            <artifactId>worldedit</artifactId>
+            <version>6.0.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 61 - 0
1_12/src/main/java/de/butzlabben/missilewars/missile/paste/r1_12/BlockFilterExtent.java

@@ -0,0 +1,61 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.missile.paste.r1_12;
+
+import com.sk89q.worldedit.Vector;
+import com.sk89q.worldedit.WorldEditException;
+import com.sk89q.worldedit.blocks.BaseBlock;
+import com.sk89q.worldedit.blocks.BlockID;
+import com.sk89q.worldedit.extent.AbstractDelegateExtent;
+import com.sk89q.worldedit.extent.Extent;
+
+/**
+ * @author Butzlabben
+ * @since 28.09.2018
+ */
+public class BlockFilterExtent extends AbstractDelegateExtent {
+
+    private byte data;
+
+    protected BlockFilterExtent(Extent extent) {
+        super(extent);
+    }
+
+    public BlockFilterExtent(Extent extent, byte data) {
+        this(extent);
+        this.data = data;
+    }
+
+    @Override
+    public BaseBlock getBlock(Vector position) {
+        BaseBlock block = super.getBlock(position);
+        if (block.getId() == BlockID.STAINED_GLASS_PANE)
+            block.setData(data);
+        if (block.getId() == BlockID.STAINED_GLASS)
+            block.setData(data);
+        return block;
+    }
+
+    @Override
+    public boolean setBlock(Vector location, BaseBlock block) throws WorldEditException {
+        if (block.getId() == 160)
+            block.setData(data);
+        return super.setBlock(location, block);
+    }
+}

+ 117 - 0
1_12/src/main/java/de/butzlabben/missilewars/missile/paste/r1_12/R1_12Paster.java

@@ -0,0 +1,117 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.missile.paste.r1_12;
+
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.Vector;
+import com.sk89q.worldedit.WorldEdit;
+import com.sk89q.worldedit.bukkit.BukkitWorld;
+import com.sk89q.worldedit.extent.Extent;
+import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
+import com.sk89q.worldedit.extent.transform.BlockTransformExtent;
+import com.sk89q.worldedit.function.mask.ExistingBlockMask;
+import com.sk89q.worldedit.function.operation.ForwardExtentCopy;
+import com.sk89q.worldedit.function.operation.Operations;
+import com.sk89q.worldedit.math.transform.AffineTransform;
+import com.sk89q.worldedit.regions.CuboidRegion;
+import com.sk89q.worldedit.world.World;
+import com.sk89q.worldedit.world.registry.WorldData;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.HashSet;
+import java.util.Set;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+
+/**
+ * @author Butzlabben
+ * @since 23.09.2018
+ */
+public class R1_12Paster {
+
+    public void pasteMissile(File schematic, org.bukkit.util.Vector pos, int rotation, org.bukkit.World world,
+                             byte data, int radius, Material replaceType, JavaPlugin plugin, int replaceTicks) {
+        try {
+            Vector position = new Vector(pos.getX(), pos.getY(), pos.getZ());
+
+            World weWorld = new BukkitWorld(world);
+            WorldData worldData = weWorld.getWorldData();
+            Clipboard clipboard = ClipboardFormat.SCHEMATIC.getReader(new FileInputStream(schematic)).read(worldData);
+            EditSession session = WorldEdit.getInstance().getEditSessionFactory().getEditSession(weWorld, -1);
+            AffineTransform transform = new AffineTransform();
+
+            transform = transform.rotateY(rotation);
+            Extent extent = new BlockTransformExtent(clipboard, transform, worldData.getBlockRegistry());
+            extent = new BlockFilterExtent(extent, data);
+
+            ForwardExtentCopy copy = new ForwardExtentCopy(extent, clipboard.getRegion(), clipboard.getOrigin(),
+                    session, position);
+
+            if (!transform.isIdentity())
+                copy.setTransform(transform);
+
+            copy.setSourceMask(new ExistingBlockMask(extent));
+
+            Operations.completeLegacy(copy);
+
+            // Replace given blocks
+            Set<Block> replace = new HashSet<>();
+            Vector min = new Vector(position.getX() - radius, position.getY() - radius, position.getZ() - radius);
+            Vector max = new Vector(position.getX() + radius, position.getY() + radius, position.getZ() + radius);
+            for (Vector v : new CuboidRegion(min, max)) {
+                Block b = world.getBlockAt(v.getBlockX(), v.getBlockY(), v.getBlockZ());
+                if (b.getType() == replaceType) {
+                    replace.add(b);
+                }
+            }
+            new BukkitRunnable() {
+                @Override
+                public void run() {
+                    replace.forEach(b -> b.setType(Material.AIR));
+                }
+            }.runTaskLater(plugin, replaceTicks);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void pasteSchematic(File schematic, org.bukkit.util.Vector pos, org.bukkit.World world) {
+        try {
+            Vector position = new Vector(pos.getX(), pos.getY(), pos.getZ());
+
+            World weWorld = new BukkitWorld(world);
+            WorldData worldData = weWorld.getWorldData();
+            Clipboard clipboard = ClipboardFormat.SCHEMATIC.getReader(new FileInputStream(schematic)).read(worldData);
+            EditSession session = WorldEdit.getInstance().getEditSessionFactory().getEditSession(weWorld, -1);
+
+            ForwardExtentCopy copy = new ForwardExtentCopy(clipboard, clipboard.getRegion(), clipboard.getOrigin(),
+                    session, position);
+
+            copy.setSourceMask(new ExistingBlockMask(clipboard));
+            Operations.completeLegacy(copy);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 55 - 0
1_13/pom.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+  ~ Copyright (c) 2018-2021 Daniel Nägele.
+  ~
+  ~ MissileWars 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.
+  ~
+  ~ MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>missilewars</artifactId>
+        <groupId>de.butzlabben</groupId>
+        <version>1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>1_13</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.sk89q.worldedit</groupId>
+            <artifactId>worldedit-bukkit</artifactId>
+            <version>7.0.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.sk89q.worldedit</groupId>
+            <artifactId>worldedit-core</artifactId>
+            <version>7.0.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.boydti</groupId>
+            <artifactId>fawe-api</artifactId>
+            <version>latest</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 51 - 0
1_13/src/main/java/de/butzlabben/missilewars/missile/paste/r1_13/we/BlockFilterExtent.java

@@ -0,0 +1,51 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.missile.paste.r1_13.we;
+
+import com.sk89q.worldedit.extent.AbstractDelegateExtent;
+import com.sk89q.worldedit.extent.Extent;
+import com.sk89q.worldedit.math.BlockVector3;
+import com.sk89q.worldedit.world.block.BaseBlock;
+import com.sk89q.worldedit.world.block.BlockTypes;
+import org.bukkit.Material;
+
+public class BlockFilterExtent extends AbstractDelegateExtent {
+
+    private Material material;
+
+    protected BlockFilterExtent(Extent extent) {
+        super(extent);
+    }
+
+    public BlockFilterExtent(Extent extent, Material material) {
+        this(extent);
+        this.material = material;
+    }
+
+    @Override
+    public BaseBlock getFullBlock(BlockVector3 position) {
+        BaseBlock block = super.getFullBlock(position);
+        if (block.getBlockType().toString().contains("stained_glass_pane")) {
+            block = BlockTypes.get(material.toString().toLowerCase() + "_pane").getDefaultState().toBaseBlock();
+        } else if (block.getBlockType().toString().contains("stained_glass")) {
+            block = BlockTypes.get(material.toString().toLowerCase()).getDefaultState().toBaseBlock();
+        }
+        return block;
+    }
+}

+ 118 - 0
1_13/src/main/java/de/butzlabben/missilewars/missile/paste/r1_13/we/R1_13Paster.java

@@ -0,0 +1,118 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.missile.paste.r1_13.we;
+
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.WorldEdit;
+import com.sk89q.worldedit.bukkit.BukkitWorld;
+import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
+import com.sk89q.worldedit.extent.transform.BlockTransformExtent;
+import com.sk89q.worldedit.function.mask.ExistingBlockMask;
+import com.sk89q.worldedit.function.operation.ForwardExtentCopy;
+import com.sk89q.worldedit.function.operation.Operation;
+import com.sk89q.worldedit.function.operation.Operations;
+import com.sk89q.worldedit.math.BlockVector3;
+import com.sk89q.worldedit.math.transform.AffineTransform;
+import com.sk89q.worldedit.regions.CuboidRegion;
+import com.sk89q.worldedit.session.ClipboardHolder;
+import com.sk89q.worldedit.world.World;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.HashSet;
+import java.util.Set;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Butzlabben
+ * @since 23.09.2018
+ */
+public class R1_13Paster {
+
+    public void pasteMissile(File schematic, org.bukkit.util.Vector pos, int rotation, org.bukkit.World world,
+                             Material glassBlockReplace, int radius, Material replaceType, JavaPlugin plugin, int replaceTicks) {
+        try {
+            World weWorld = new BukkitWorld(world);
+            ClipboardFormat format = ClipboardFormats.findByFile(schematic);
+
+            try (ClipboardReader reader = format.getReader(new FileInputStream(schematic));
+                 EditSession editSession = WorldEdit.getInstance().getEditSessionFactory().getEditSession(weWorld, -1)) {
+                ClipboardHolder clipboardHolder = new ClipboardHolder(reader.read());
+                Clipboard clipboard = clipboardHolder.getClipboard();
+                AffineTransform transform = new AffineTransform();
+                transform = transform.rotateY(rotation);
+
+                BlockTransformExtent extent = new BlockTransformExtent(clipboard, transform);
+                ForwardExtentCopy copy = new ForwardExtentCopy(new BlockFilterExtent(extent, glassBlockReplace), clipboard.getRegion(), clipboard.getOrigin(), editSession, BlockVector3.at(pos.getX(), pos.getY(), pos.getZ()));
+                copy.setTransform(transform);
+                copy.setSourceMask(new ExistingBlockMask(clipboard));
+
+                Operations.complete(copy);
+            }
+
+            // Replace given blocks
+            Set<Block> replace = new HashSet<>();
+            BlockVector3 min = BlockVector3.at(pos.getX() - radius, pos.getY() - radius, pos.getZ() - radius);
+            BlockVector3 max = BlockVector3.at(pos.getX() + radius, pos.getY() + radius, pos.getZ() + radius);
+            for (BlockVector3 v : new CuboidRegion(min, max)) {
+                Block b = world.getBlockAt(v.getBlockX(), v.getBlockY(), v.getBlockZ());
+                if (b.getType() == replaceType) {
+                    replace.add(b);
+                }
+            }
+            new BukkitRunnable() {
+                @Override
+                public void run() {
+                    replace.forEach(b -> b.setType(Material.AIR));
+                }
+            }.runTaskLater(plugin, replaceTicks);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void pasteSchematic(File schematic, Vector pos, org.bukkit.World world) {
+        try {
+            World weWorld = new BukkitWorld(world);
+
+            ClipboardFormat format = ClipboardFormats.findByFile(schematic);
+            try (ClipboardReader reader = format.getReader(new FileInputStream(schematic));
+                 EditSession editSession = WorldEdit.getInstance().getEditSessionFactory().getEditSession(weWorld, -1)) {
+                ClipboardHolder clipboard = new ClipboardHolder(reader.read());
+
+                Operation operation = clipboard
+                        .createPaste(editSession)
+                        .to(BlockVector3.at(pos.getX(), pos.getY(), pos.getZ()))
+                        .ignoreAirBlocks(true)
+                        .build();
+                Operations.complete(operation);
+
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

BIN
1_13_FAWE/lib/FastAsyncWorldEdit.jar


+ 43 - 0
1_13_FAWE/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+  ~ Copyright (c) 2018-2021 Daniel Nägele.
+  ~
+  ~ MissileWars 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.
+  ~
+  ~ MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>missilewars</artifactId>
+        <groupId>de.butzlabben</groupId>
+        <version>1.0</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>1_13_FAWE</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.sk98q.worldedit</groupId>
+            <artifactId>FastAsnycWorldEdit</artifactId>
+            <scope>system</scope>
+            <version>1.0</version>
+            <systemPath>${pom.basedir}/lib/FastAsyncWorldEdit.jar</systemPath>
+        </dependency>
+    </dependencies>
+
+</project>

+ 97 - 0
1_13_FAWE/src/main/java/de/butzlabben/missilewars/missile/paste/r1_13/fawe/R1_13Paster.java

@@ -0,0 +1,97 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.missile.paste.r1_13.fawe;
+
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.Vector;
+import com.sk89q.worldedit.WorldEdit;
+import com.sk89q.worldedit.bukkit.BukkitWorld;
+import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
+import com.sk89q.worldedit.function.mask.ExistingBlockMask;
+import com.sk89q.worldedit.function.operation.ForwardExtentCopy;
+import com.sk89q.worldedit.function.operation.Operations;
+import com.sk89q.worldedit.math.transform.AffineTransform;
+import com.sk89q.worldedit.regions.CuboidRegion;
+import com.sk89q.worldedit.world.World;
+import java.io.File;
+import java.util.HashSet;
+import java.util.Set;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+
+public class R1_13Paster {
+
+    public void pasteMissile(File schematic, org.bukkit.util.Vector pos, int rotation, org.bukkit.World world,
+                             Material glassBlockReplace, int radius, Material replaceType, JavaPlugin plugin, int replaceTicks) {
+        try {
+            World weWorld = new BukkitWorld(world);
+
+            EditSession editSession = WorldEdit.getInstance().getEditSessionFactory().getEditSession(weWorld, -1);
+            Clipboard clipboard = ClipboardFormat.findByFile(schematic).load(schematic).getClipboard();
+
+            AffineTransform transform = new AffineTransform();
+            transform = transform.rotateY(rotation);
+
+            Vector origin = new Vector(clipboard.getOrigin().getX(), clipboard.getOrigin().getY(), clipboard.getOrigin().getZ());
+//            BlockTransformExtent extent = new BlockTransformExtent(clipboard, transform, ((BukkitWorld) weWorld).getWorldData().getBlockRegistry());
+            ForwardExtentCopy copy = new ForwardExtentCopy(clipboard, clipboard.getRegion(), origin, editSession, new Vector(pos.getX(), pos.getY(), pos.getZ()));
+            copy.setTransform(transform);
+            copy.setSourceMask(new ExistingBlockMask(clipboard));
+
+            Operations.complete(copy);
+
+
+            // Replace given blocks
+            Set<Block> replace = new HashSet<>();
+            Vector min = new Vector(pos.getX() - radius, pos.getY() - radius, pos.getZ() - radius);
+            Vector max = new Vector(pos.getX() + radius, pos.getY() + radius, pos.getZ() + radius);
+            for (Vector v : new CuboidRegion(min, max)) {
+                Block b = world.getBlockAt(v.getBlockX(), v.getBlockY(), v.getBlockZ());
+                if (b.getType() == replaceType) {
+                    replace.add(b);
+                }
+            }
+            new BukkitRunnable() {
+                @Override
+                public void run() {
+                    replace.forEach(b -> b.setType(Material.AIR));
+                }
+            }.runTaskLater(plugin, replaceTicks);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void pasteSchematic(File schematic, org.bukkit.util.Vector pos, org.bukkit.World world) {
+        try {
+            World weWorld = new BukkitWorld(world);
+
+            EditSession editSession = ClipboardFormat.findByFile(schematic).load(schematic)
+                    .paste(weWorld, new Vector(pos.getX(), pos.getY(), pos.getZ()), false, false, null);
+            editSession.flushQueue();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 43 - 0
1_16_FAWE/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+  ~ Copyright (c) 2018-2021 Daniel Nägele.
+  ~
+  ~ MissileWars 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.
+  ~
+  ~ MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>missilewars</artifactId>
+        <groupId>de.butzlabben</groupId>
+        <version>1.0</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>1_16_FAWE</artifactId>
+
+    <!-- FAWE API -->
+    <dependencies>
+        <dependency>
+            <groupId>com.intellectualsites.fawe</groupId>
+            <artifactId>FAWE-Bukkit</artifactId>
+            <version>1.16-583</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 93 - 0
1_16_FAWE/src/main/java/de/butzlabben/missilewars/missile/paste/r1_16/fawe/R1_16Paster.java

@@ -0,0 +1,93 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.missile.paste.r1_16.fawe;
+
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.bukkit.BukkitWorld;
+import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
+import com.sk89q.worldedit.math.BlockVector3;
+import com.sk89q.worldedit.math.transform.AffineTransform;
+import com.sk89q.worldedit.regions.CuboidRegion;
+import com.sk89q.worldedit.world.World;
+import java.io.File;
+import java.util.HashSet;
+import java.util.Set;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Daniel Nägele
+ */
+public class R1_16Paster {
+
+    public void pasteMissile(File schematic, Vector pos, int rotation, org.bukkit.World world,
+                             Material glassBlockReplace, int radius, Material replaceType, JavaPlugin plugin, int replaceTicks) {
+        try {
+            World weWorld = new BukkitWorld(world);
+
+            Clipboard clipboard = ClipboardFormats.findByFile(schematic).load(schematic);
+
+            AffineTransform transform = new AffineTransform();
+            transform = transform.rotateY(rotation);
+
+            clipboard.paste(weWorld, fromBukkitVector(pos), false, false, transform);
+
+            // Replace given blocks
+            Set<Block> replace = new HashSet<>();
+            Vector min = new Vector(pos.getX() - radius, pos.getY() - radius, pos.getZ() - radius);
+            Vector max = new Vector(pos.getX() + radius, pos.getY() + radius, pos.getZ() + radius);
+            for (BlockVector3 v : new CuboidRegion(fromBukkitVector(min), fromBukkitVector(max))) {
+                Block b = world.getBlockAt(v.getBlockX(), v.getBlockY(), v.getBlockZ());
+                if (b.getType() == replaceType) {
+                    replace.add(b);
+                }
+            }
+            new BukkitRunnable() {
+                @Override
+                public void run() {
+                    replace.forEach(b -> b.setType(Material.AIR));
+                }
+            }.runTaskLater(plugin, replaceTicks);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void pasteSchematic(File schematic, Vector pos, org.bukkit.World world) {
+        try {
+            World weWorld = new BukkitWorld(world);
+
+            EditSession editSession = ClipboardFormats.findByFile(schematic).load(schematic)
+                    .paste(weWorld, fromBukkitVector(pos), false, false, null);
+            editSession.flushQueue();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private BlockVector3 fromBukkitVector(org.bukkit.util.Vector pos) {
+        return BlockVector3.at(pos.getX(), pos.getY(), pos.getZ());
+    }
+}

+ 675 - 0
LICENSE

@@ -0,0 +1,675 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
+

+ 124 - 0
missilewars-plugin/pom.xml

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+  ~ Copyright (c) 2018-2021 Daniel Nägele.
+  ~
+  ~ MissileWars 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.
+  ~
+  ~ MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>missilewars</artifactId>
+        <groupId>de.butzlabben</groupId>
+        <version>1.0</version>
+    </parent>
+
+    <version>4.1.0</version>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>missilewars-plugin</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.butzlabben</groupId>
+            <artifactId>1_12</artifactId>
+            <version>1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>de.butzlabben</groupId>
+            <artifactId>1_13</artifactId>
+            <version>1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>de.butzlabben</groupId>
+            <artifactId>1_13_FAWE</artifactId>
+            <version>1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>de.butzlabben</groupId>
+            <artifactId>1_16_FAWE</artifactId>
+            <version>1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <!-- Other dependencies -->
+
+        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.11.0</version>
+            <scope>compile</scope>
+        </dependency>
+
+
+        <!-- https://mvnrepository.com/artifact/com.pro-crafting.mc/commandframework -->
+        <dependency>
+            <groupId>com.pro-crafting.mc</groupId>
+            <artifactId>commandframework</artifactId>
+            <version>0.2.1</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.12.4</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.12.4</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-yaml</artifactId>
+            <version>2.12.4</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.bstats</groupId>
+            <artifactId>bstats-bukkit</artifactId>
+            <version>2.2.1</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>MissileWars-${project.version}</finalName>
+        <defaultGoal>package</defaultGoal>
+        <sourceDirectory>src/main/java</sourceDirectory>
+    </build>
+
+</project>

+ 293 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/Config.java

@@ -0,0 +1,293 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars;
+
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.util.SetupUtil;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import static org.bukkit.Material.JUKEBOX;
+import static org.bukkit.Material.valueOf;
+import org.bukkit.World;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+public class Config {
+
+    private static final File dir = MissileWars.getInstance().getDataFolder();
+    private static final File file = new File(MissileWars.getInstance().getDataFolder(), "config.yml");
+    private static YamlConfiguration cfg;
+
+    public static void load() {
+        boolean configNew = false;
+
+        if (!dir.exists())
+            dir.mkdirs();
+
+        SetupUtil.checkMissiles();
+        if (!file.exists()) {
+            configNew = true;
+            dir.mkdirs();
+            try {
+                file.createNewFile();
+            } catch (IOException e) {
+                Logger.ERROR.log("Could not create config!");
+                e.printStackTrace();
+            }
+        }
+
+        try {
+            cfg = YamlConfiguration
+                    .loadConfiguration(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
+        } catch (FileNotFoundException e1) {
+            Logger.ERROR.log("Couldn't load config.yml");
+            e1.printStackTrace();
+            return;
+        }
+
+        cfg.options().copyDefaults(true);
+
+        cfg.addDefault("debug", false);
+        if (debug())
+            Logger.DEBUG.log("Debug enabled");
+
+        cfg.addDefault("setup_mode", false);
+
+        cfg.addDefault("contact_auth_server", true);
+        cfg.addDefault("prefetch_players", true);
+
+        cfg.addDefault("restart_after_fights", 10);
+
+        cfg.addDefault("arena_folder", "plugins/MissileWars/arenas");
+
+        cfg.addDefault("lobbies.multiple_lobbies", false);
+        cfg.addDefault("lobbies.folder", "plugins/MissileWars/lobbies");
+        cfg.addDefault("lobbies.default_lobby", "lobby0.yml");
+
+        cfg.addDefault("replace.material", JUKEBOX.name());
+        cfg.addDefault("replace.after_ticks", 2);
+        cfg.addDefault("replace.radius", 15);
+
+        cfg.addDefault("motd.enable", true);
+        cfg.addDefault("motd.lobby", "&6•&e● MissileWars &7| &eLobby");
+        cfg.addDefault("motd.ingame", "&6•&e● MissileWars &7| &bIngame");
+        cfg.addDefault("motd.ended", "&6•&e● MissileWars &7| &cRestarting...");
+
+        cfg.addDefault("fightstats.enable", false);
+        cfg.addDefault("fightstats.show_real_skins", true);
+
+        Location spawnLocation = Bukkit.getWorlds().get(0).getSpawnLocation().add(25, 0, 25);
+        cfg.addDefault("fallback_spawn.world", spawnLocation.getWorld().getName());
+        cfg.addDefault("fallback_spawn.x", spawnLocation.getX());
+        cfg.addDefault("fallback_spawn.y", spawnLocation.getY());
+        cfg.addDefault("fallback_spawn.z", spawnLocation.getZ());
+        cfg.addDefault("fallback_spawn.yaw", spawnLocation.getYaw());
+        cfg.addDefault("fallback_spawn.pitch", spawnLocation.getPitch());
+
+        cfg.addDefault("mysql.host", "localhost");
+        cfg.addDefault("mysql.database", "db");
+        cfg.addDefault("mysql.port", "3306");
+        cfg.addDefault("mysql.user", "root");
+        cfg.addDefault("mysql.password", "");
+        cfg.addDefault("mysql.fights_table", "mw_fights");
+        cfg.addDefault("mysql.fightmember_table", "mw_fightmember");
+
+        cfg.addDefault("sidebar.title", "§eInfo ●§6•");
+        if (configNew) {
+            cfg.addDefault("sidebar.entries.6", "§7Time left:");
+            cfg.addDefault("sidebar.entries.5", "§e» %time%m");
+
+            cfg.addDefault("sidebar.entries.4", "  ");
+            cfg.addDefault("sidebar.entries.3", "%team1% §7» %team1_color%%team1_amount%");
+
+            cfg.addDefault("sidebar.entries.2", "   ");
+            cfg.addDefault("sidebar.entries.1", "%team2% §7» %team2_color%%team2_amount%");
+        }
+        try {
+            cfg.save(file);
+        } catch (IOException e) {
+            Logger.ERROR.log("Could not save config!");
+            e.printStackTrace();
+        }
+    }
+
+    public static HashMap<String, Integer> getScoreboardEntries() {
+        HashMap<String, Integer> ret = new HashMap<>();
+        ConfigurationSection section = cfg.getConfigurationSection("sidebar.entries");
+        for (String s : section.getKeys(false)) {
+            ret.put(section.getString(s), Integer.valueOf(s));
+        }
+        return ret;
+    }
+
+    public static Material getStartReplace() {
+        String name = cfg.getString("replace.material", "JUKEBOX").toUpperCase();
+        try {
+            return valueOf(name);
+        } catch (Exception e) {
+            Logger.WARN.log("Unknown material " + name + " in start_replace");
+        }
+        return null;
+    }
+
+    public static void save(YamlConfiguration cfg) {
+        try {
+            cfg.save(file);
+        } catch (IOException e) {
+            Logger.ERROR.log("Couldn't save config");
+            e.printStackTrace();
+        }
+    }
+
+    public static Location getFallbackSpawn() {
+        ConfigurationSection cfg = Config.cfg.getConfigurationSection("fallback_spawn");
+        World world = Bukkit.getWorld(cfg.getString("world"));
+        if (world == null) {
+            Logger.WARN.log("The world configured at \"fallback_location.world\" couldn't be found. Using the default one");
+            world = Bukkit.getWorlds().get(0);
+        }
+        Location location = new Location(world,
+                cfg.getDouble("x"),
+                cfg.getDouble("y"),
+                cfg.getDouble("z"),
+                (float) cfg.getDouble("yaw"),
+                (float) cfg.getDouble("pitch"));
+        if (GameManager.getInstance().getGame(location) != null) {
+            Logger.WARN.log("Your fallback spawn is inside a game area. This plugins functionality can no longer be guaranteed");
+        }
+
+        return location;
+    }
+
+    public static String motdEnded() {
+        return cfg.getString("motd.ended", "&cError configuring motd in motd.ended");
+    }
+
+    public static String motdGame() {
+        return cfg.getString("motd.ingame", "&cError configuring motd in motd.ingame");
+    }
+
+    public static String motdLobby() {
+        return cfg.getString("motd.lobby", "&cError configuring motd in motd.lobby");
+    }
+
+    public static boolean motdEnabled() {
+        return cfg.getBoolean("motd.enable", true);
+    }
+
+    public static int getReplaceTicks() {
+        return cfg.getInt("replace.after_ticks", 1);
+    }
+
+    public static int getReplaceRadius() {
+        return cfg.getInt("replace.radius", 1);
+    }
+
+    static boolean debug() {
+        return cfg.getBoolean("debug");
+    }
+
+    public static YamlConfiguration getConfig() {
+        return cfg;
+    }
+
+    public static boolean isSetup() {
+        return cfg.getBoolean("setup_mode");
+    }
+
+    public static int getFightRestart() {
+        return cfg.getInt("restart_after_fights");
+    }
+
+    public static String getScoreboardTitle() {
+        return cfg.getString("sidebar.title");
+    }
+
+    public static String getHost() {
+        return cfg.getString("mysql.host", "localhost");
+    }
+
+    public static String getDatabase() {
+        return cfg.getString("mysql.database", "db");
+    }
+
+    public static String getPort() {
+        return cfg.getString("mysql.port", "3306");
+    }
+
+    public static String getUser() {
+        return cfg.getString("mysql.user", "root");
+    }
+
+    public static String getPassword() {
+        return cfg.getString("mysql.password", "");
+    }
+
+    public static String getFightsTable() {
+        return cfg.getString("mysql.fights_table", "mw_fights");
+    }
+
+    public static String getFightMembersTable() {
+        return cfg.getString("mysql.fightmember_table", "mw_fightmember");
+    }
+
+    public static String getArenaFolder() {
+        return cfg.getString("arena_folder") + "/";
+    }
+
+    public static boolean isContactAuth() {
+        return cfg.getBoolean("contact_auth_server");
+    }
+
+    public static boolean isPrefetchPlayers() {
+        return cfg.getBoolean("prefetch_players");
+    }
+
+    public static boolean isShowRealSkins() {
+        return cfg.getBoolean("fightstats.show_real_skins");
+    }
+
+    public static boolean isMultipleLobbies() {
+        return cfg.getBoolean("lobbies.multiple_lobbies");
+    }
+
+    public static String getLobbiesFolder() {
+        return cfg.getString("lobbies.folder") + "/";
+    }
+
+    public static String getDefaultLobby() {
+        return cfg.getString("lobbies.default_lobby");
+    }
+
+    public static boolean isFightStatsEnabled() {
+        return cfg.getBoolean("fightstats.enable");
+    }
+}

+ 51 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/Logger.java

@@ -0,0 +1,51 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars;
+
+import org.bukkit.Bukkit;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+public enum Logger {
+
+    BOOT("[MW] Boot | "),
+    SUCCESS("[MW] Success | "),
+    NORMAL("[MW] "),
+    DEBUG("[MW] Debug | "),
+    BOOTDONE("[MW] Boot | "),
+    WARN("[MW] Warn | "),
+    ERROR("[MW] Error | ");
+
+    private final String prefix;
+
+    Logger(String prefix) {
+        this.prefix = prefix;
+    }
+
+    public void log(String msg) {
+        if (this == DEBUG && !Config.debug()) {
+            return;
+        }
+        if (this == BOOTDONE)
+            msg = msg + " [§aDONE§r]";
+        Bukkit.getConsoleSender().sendMessage(prefix + msg);
+    }
+}

+ 134 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/MessageConfig.java

@@ -0,0 +1,134 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import org.bukkit.ChatColor;
+import org.bukkit.configuration.file.YamlConfiguration;
+
+
+/**
+ * @author Butzlabben
+ * @since 13.08.2018
+ */
+public class MessageConfig {
+
+    private static final File dir = new File(MissileWars.getInstance().getDataFolder(), "missiles/");
+    private static final File file = new File(MissileWars.getInstance().getDataFolder(), "messages.yml");
+    private static YamlConfiguration cfg;
+
+    public static void load() {
+        if (!file.exists()) {
+            dir.mkdirs();
+            try {
+                file.createNewFile();
+            } catch (IOException e) {
+                Logger.ERROR.log("Could not create properties!");
+                e.printStackTrace();
+            }
+        }
+        try {
+            cfg = YamlConfiguration.loadConfiguration(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
+        } catch (FileNotFoundException e1) {
+            Logger.ERROR.log("Couldn't load messages.yml");
+            e1.printStackTrace();
+            return;
+        }
+
+        cfg.options().copyDefaults(true);
+
+        cfg.addDefault("prefix", "&6•&e● MissileWars &8▎  &7");
+
+        cfg.addDefault("not_in_arena", "&cYou are not in an arena right now");
+        cfg.addDefault("game_quit", "You lef this game");
+        cfg.addDefault("not_enter_arena", "&cYou may not enter this arena right now");
+        cfg.addDefault("game_starts_new_in", "Game starts new in &e%seconds% &7seconds");
+        cfg.addDefault("game_ends_in_minutes", "Game ends in &e%minutes% &7minutes");
+        cfg.addDefault("game_ends_in_seconds", "Game ends in &e%seconds% &7seconds");
+        cfg.addDefault("game_starts_in", "Game starts in &e%seconds% &7seconds");
+        cfg.addDefault("not_enough_players", "&cThere are not enough players online");
+        cfg.addDefault("teams_unequal", "&cThe teams are unequal distributed");
+        cfg.addDefault("game_starts", "&aThe game starts");
+        cfg.addDefault("fall_protection", "&cFall protection inactive in %seconds% seconds");
+        cfg.addDefault("fall_protection_inactive", "&cFall protection inactive");
+        cfg.addDefault("fall_protection_deactivated", "&cFall protection deactivated by sneaking");
+        cfg.addDefault("money", "You received &e%money% &7coins");
+        cfg.addDefault("kick_inactivity", "&cYou were inactive on missilewars");
+        cfg.addDefault("title_won", "%team%");
+        cfg.addDefault("subtitle_won", "&7has won the game");
+        cfg.addDefault("spectator", "&7You are now a spectator");
+        cfg.addDefault("change_team_not_now", "&cNow you cannot change your team anymore");
+        cfg.addDefault("already_in_team", "&cYou are already in this team");
+        cfg.addDefault("cannot_change_difference", "&cYou cannot change your team");
+        cfg.addDefault("team_changed", "You are now in %team%");
+        cfg.addDefault("team_assigned", "You have been assigned to %team%");
+        cfg.addDefault("lobby_joined", "&e%player% &7joined &8(&7%players%&8/&7%max_players%&8)");
+        cfg.addDefault("not_higher", "&cYou can not go higher");
+        cfg.addDefault("invalid_missile", "&cInvalid missile");
+        cfg.addDefault("hurt_teammates", "&cYou must not hurt your teammates");
+        cfg.addDefault("died", "%player% &7died");
+        cfg.addDefault("died_explosion", "%player% &7was blown up");
+        cfg.addDefault("player_left", "%player% &7left the game");
+        cfg.addDefault("team_offline", "Everyone from %team% &7is offline");
+        cfg.addDefault("team_buffed", "%team% &7was buffed as one player left the team");
+        cfg.addDefault("team_nerved", "%team% &7was nerved as one player joined the team");
+        cfg.addDefault("restart_after_game", "&7The server will restart after this game");
+        cfg.addDefault("arena_leave", "&cYou are not allowed to leave the arena");
+        cfg.addDefault("missile_place_deny", "&cYou are not allowed to place a missile here");
+        cfg.addDefault("sign.0", "•● MissileWars ●•");
+        cfg.addDefault("sign.1", "%state%");
+        cfg.addDefault("sign.2", "%arena%");
+        cfg.addDefault("sign.3", "%players%/%max_players%");
+        cfg.addDefault("sign.state.lobby", "&aLobby");
+        cfg.addDefault("sign.state.ingame", "&bIngame");
+        cfg.addDefault("sign.state.ended", "&cRestarting...");
+        cfg.addDefault("sign.state.error", "&cError...");
+        cfg.addDefault("vote.success", "You successfully voted for the map %map%");
+        cfg.addDefault("vote.finished", "The map %map% &7was elected");
+        cfg.addDefault("vote.gui", "Vote for a map");
+
+        try {
+            cfg.save(file);
+        } catch (IOException e) {
+            Logger.ERROR.log("Could not save properties!");
+            e.printStackTrace();
+        }
+    }
+
+    public static String getMessage(String path) {
+        return getPrefix() + getNativeMessage(path);
+    }
+
+    public static String getNativeMessage(String path) {
+        return ChatColor.translateAlternateColorCodes('&', getRawMessage(path));
+    }
+
+    private static String getRawMessage(String path) {
+        return cfg.getString(path, "&cError while reading from messages.yml: " + path);
+    }
+
+    public static String getPrefix() {
+        return ChatColor.translateAlternateColorCodes('&', cfg.getString("prefix", "&6•&e● MissileWars &8▎  &7"));
+    }
+}

+ 182 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/MissileWars.java

@@ -0,0 +1,182 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars;
+
+import com.pro_crafting.mc.commandframework.CommandFramework;
+import de.butzlabben.missilewars.cmd.MWCommands;
+import de.butzlabben.missilewars.cmd.StatsCommands;
+import de.butzlabben.missilewars.cmd.UserCommands;
+import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.listener.PlayerListener;
+import de.butzlabben.missilewars.listener.signs.ClickListener;
+import de.butzlabben.missilewars.listener.signs.ManageListener;
+import de.butzlabben.missilewars.util.ConnectionHolder;
+import de.butzlabben.missilewars.util.MoneyUtil;
+import de.butzlabben.missilewars.util.SetupUtil;
+import de.butzlabben.missilewars.util.stats.PreFetcher;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.signs.CheckRunnable;
+import de.butzlabben.missilewars.wrapper.signs.SignRepository;
+import de.butzlabben.missilewars.wrapper.stats.StatsFetcher;
+import java.io.File;
+import java.util.Date;
+import lombok.Getter;
+import org.apache.commons.io.FileUtils;
+import org.bstats.bukkit.Metrics;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+@Getter
+public class MissileWars extends JavaPlugin {
+
+    private static MissileWars instance;
+    public final String version = getDescription().getVersion();
+    private CommandFramework framework;
+    private SignRepository signRepository;
+
+    private boolean foundFAWE;
+
+    public MissileWars() {
+        instance = this;
+    }
+
+    /**
+     * @return the instance
+     */
+    public static MissileWars getInstance() {
+        return instance;
+    }
+
+    @Override
+    public void onEnable() {
+        long start = System.currentTimeMillis();
+        Logger.BOOT.log("This server is running MissileWars v" + version + " by Butzlabben");
+        if (VersionUtil.getVersion() < 8) {
+            Logger.WARN.log("====================================================");
+            Logger.WARN.log("It seems that you are using version older than 1.8");
+            Logger.WARN.log("There is no guarantee for this to work");
+            Logger.WARN.log("Proceed with extreme caution");
+            Logger.WARN.log("====================================================");
+        }
+
+        if (version.contains("beta")) {
+            Logger.WARN.log("NOTE: This is a beta version which means, that it may not be fully stable");
+        }
+
+        if (getDescription().getAuthors().size() > 1) {
+            StringBuilder sb = new StringBuilder();
+            for (String author : getDescription().getAuthors()) {
+                if (author.equals("Butzlabben"))
+                    continue;
+                sb.append(author);
+                sb.append(" ");
+            }
+            Logger.BOOT.log("Other authors: " + sb);
+        }
+
+        Logger.BOOT.log("Loading properties...");
+        checkMaps();
+        Config.load();
+        MessageConfig.load();
+        // I don't know why, and I don't want to know why, but this is needed to ensure the the messages are properly loaded at the first time
+        MessageConfig.load();
+        new File(Config.getArenaFolder()).mkdirs();
+        new File(Config.getLobbiesFolder()).mkdirs();
+
+        SignRepository repository = SignRepository.load();
+        if (repository == null) {
+            repository = new SignRepository();
+            repository.save();
+        }
+        this.signRepository = repository;
+
+        checkMaps();
+
+        Bukkit.getPluginManager().registerEvents(new PlayerListener(), this);
+        Bukkit.getPluginManager().registerEvents(new ClickListener(), this);
+        Bukkit.getPluginManager().registerEvents(new ManageListener(), this);
+
+        Logger.BOOT.log("Registering commands");
+        framework = new CommandFramework(this);
+        framework.registerCommands(new MWCommands());
+        framework.registerCommands(new StatsCommands());
+        framework.registerCommands(new UserCommands());
+        Logger.BOOTDONE.log("Registering commands");
+
+        Arenas.load();
+        SetupUtil.checkShields();
+
+        GameManager.getInstance().loadGames();
+
+        new Metrics(this, 3749);
+
+        foundFAWE = Bukkit.getPluginManager().getPlugin("FastAsyncWorldEdit") != null;
+
+        GameManager.getInstance().getGames().values().forEach(game -> {
+            for (Player player : Bukkit.getOnlinePlayers()) {
+                if (!game.isIn(player.getLocation())) continue;
+                game.addPlayer(player);
+            }
+        });
+
+        MoneyUtil.giveMoney(null, -1);
+
+        Bukkit.getScheduler().runTaskTimerAsynchronously(this, new CheckRunnable(), 20, 20 * 10);
+
+        if (Config.isPrefetchPlayers()) {
+            PreFetcher.preFetchPlayers(new StatsFetcher(new Date(0L), ""));
+        }
+
+        long end = System.currentTimeMillis();
+        Logger.SUCCESS.log("MissileWars was enabled in " + (end - start) + "ms");
+    }
+
+    @Override
+    public void onDisable() {
+        GameManager.getInstance().disableAll();
+        checkMaps();
+        File missiles = new File(getDataFolder(), "missiles.zip");
+        File arena = new File(getDataFolder(), "MissileWars-Arena.zip");
+        FileUtils.deleteQuietly(missiles);
+        FileUtils.deleteQuietly(arena);
+        ConnectionHolder.close();
+    }
+
+    public boolean foundFAWE() {
+        return foundFAWE;
+    }
+
+    private void checkMaps() {
+        File[] dirs = Bukkit.getWorldContainer().listFiles();
+        for (File dir : dirs) {
+            if (dir.getName().startsWith("mw-")) {
+                try {
+                    FileUtils.deleteDirectory(dir);
+                } catch (Exception ignored) {
+                }
+            }
+        }
+    }
+}

+ 226 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/cmd/MWCommands.java

@@ -0,0 +1,226 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.cmd;
+
+import com.pro_crafting.mc.commandframework.Command;
+import com.pro_crafting.mc.commandframework.CommandArgs;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.game.GameState;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import de.butzlabben.missilewars.wrapper.abstracts.Lobby;
+import de.butzlabben.missilewars.wrapper.abstracts.MapChooseProcedure;
+import de.butzlabben.missilewars.wrapper.missile.Missile;
+import de.butzlabben.missilewars.wrapper.missile.MissileFacing;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class MWCommands {
+
+    @Command(name = "mw.paste", usage = "/mw paste <missile>", permission = "mw.paste", description = "Pastes a missile", inGameOnly = true)
+    public void pasteCommand(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player p = (Player) sender;
+        Game game = GameManager.getInstance().getGame(p.getLocation());
+        if (game == null) {
+            p.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < args.length(); i++) {
+            sb.append(args.getArgs(i).replaceAll("&.", ""));
+            sb.append(" ");
+        }
+        Missile m = game.getArena().getMissileConfiguration().getMissileFromName(sb.toString().trim());
+        if (m == null) {
+            p.sendMessage(MessageConfig.getPrefix() + "§cUnknown missile");
+            return;
+        }
+        MissileFacing mf = MissileFacing.getFacingPlayer(p, game.getArena().getMissileConfiguration());
+        m.paste(p, mf, game);
+    }
+
+    @Command(name = "mw.start", usage = "/mw start", permission = "mw.start", description = "Starts the game", inGameOnly = true)
+    public void startCommand(CommandArgs args) {
+        CommandSender cs = args.getSender();
+        if (!(cs instanceof Player)) {
+            cs.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player sender = (Player) cs;
+        Game game = GameManager.getInstance().getGame(sender.getLocation());
+
+        if (game == null) {
+            sender.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+
+        if (game.getState() != GameState.LOBBY) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cGame already started");
+            return;
+        }
+        if (game.isReady())
+            game.startGame();
+        else {
+            if (game.getLobby().getMapChooseProcedure() != MapChooseProcedure.MAPVOTING && game.getArena() == null) {
+                sender.sendMessage(MessageConfig.getPrefix() + "§cGame cannot be started");
+            } else {
+                Map.Entry<String, Integer> mostVotes = null;
+                for (Map.Entry<String, Integer> arena : game.getVotes().entrySet()) {
+                    if (mostVotes == null) {
+                        mostVotes = arena;
+                        continue;
+                    }
+                    if (arena.getValue() > mostVotes.getValue()) mostVotes = arena;
+                }
+                if (mostVotes == null) throw new IllegalStateException("Most votes object was null");
+                Optional<Arena> arena = Arenas.getFromName(mostVotes.getKey());
+                if (!arena.isPresent()) throw new IllegalStateException("Voted arena is not present");
+                game.setArena(arena.get());
+                sender.sendMessage(MessageConfig.getPrefix() + "A map was elected. Use \"/mw start\" again to start the round");
+            }
+        }
+    }
+
+    @Command(name = "mw.stop", usage = "/mw stop", permission = "mw.stop", description = "Stops the game", inGameOnly = true)
+    public void stopCommand(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+        if (game == null) {
+            player.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+
+        Bukkit.getScheduler().runTask(MissileWars.getInstance(), game::stopGame);
+    }
+
+
+    @Command(name = "mw.restart", usage = "/mw restart", permission = "mw.restart", description = "Restarts the game", inGameOnly = true)
+    public void restartCommand(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+
+        if (game == null) {
+            player.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+
+        Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> {
+            if (game.getState() == GameState.INGAME)
+                game.stopGame();
+            game.reset();
+        });
+    }
+
+    @Command(name = "mw.appendrestart", usage = "/mw appendrestart", permission = "mw.appendrestart", description = "Appends a restart after the next game ends")
+    public void appendRestartCommand(CommandArgs args) {
+        GameManager.getInstance().getGames().values().forEach(Game::appendRestart);
+        args.getSender().sendMessage(MessageConfig.getMessage("restart_after_game"));
+    }
+
+    @Command(name = "mw", aliases = "missilewars", usage = "/mw", description = "Shows information about the MissileWars Plugin")
+    public void mwCommand(CommandArgs args) {
+        CommandSender cs = args.getSender();
+
+        cs.sendMessage(MessageConfig.getPrefix() + "MissileWars v" + MissileWars.getInstance().version + " by Butzlabben");
+
+        if (cs.hasPermission("mw.quit"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw quit -  Quit a game");
+        if (cs.hasPermission("mw.start"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw start - Starts the game");
+        if (cs.hasPermission("mw.stop"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw stop - Stops the game");
+        if (cs.hasPermission("mw.restart"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw start - Restarts the game");
+        if (cs.hasPermission("mw.appendrestart"))
+            cs.sendMessage(MessageConfig.getPrefix()
+                    + "/mw appendrestart - Appends a restart after the next game ends");
+        if (cs.hasPermission("mw.paste"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw paste - Pastes a missile");
+        if (cs.hasPermission("mw.reload"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw reload - Reloads configurations");
+        if (cs.hasPermission("mw.stats"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw stats - Shows stats");
+        if (cs.hasPermission("mw.stats.recommendations"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw stats recommendations - Shows recommendations");
+        if (cs.hasPermission("mw.stats.players"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw stats players - Shows player list");
+        if (cs.hasPermission("mw.stats.list"))
+            cs.sendMessage(MessageConfig.getPrefix() + "/mw stats list - Lists history of games");
+    }
+
+    @Command(name = "mw.reload", permission = "mw.reload", usage = "/mw reload")
+    public void onReload(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        Config.load();
+        MessageConfig.load();
+        Arenas.load();
+        sender.sendMessage(MessageConfig.getPrefix() + "Reloaded configs");
+    }
+
+    @Command(name = "mw.debug", permission = "mw.debug", usage = "/mw debug")
+    public void onDebug(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        int i = 0;
+        Logger.NORMAL.log("Starting to print debug information for MissileWars v" + MissileWars.getInstance().version);
+        for (Game game : GameManager.getInstance().getGames().values()) {
+            Logger.NORMAL.log("Printing state for arena " + game.getArena().getName() + ". Number: " + i);
+            Logger.NORMAL.log(game.toString());
+        }
+        sender.sendMessage(MessageConfig.getPrefix() + "Printed debug message into the log file");
+    }
+
+    @Command(name = "mw.restartall", permission = "mw.reload", usage = "/mw restartall")
+    public void onRestartAll(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        sender.sendMessage(MessageConfig.getPrefix() + "§cWarning - Restarting all games. This may take a while");
+        List<Lobby> arenaPropertiesList = GameManager.getInstance().getGames().values()
+                .stream().map(Game::getLobby).collect(Collectors.toList());
+        arenaPropertiesList.forEach(GameManager.getInstance()::restartGame);
+        sender.sendMessage(MessageConfig.getPrefix() + "Reloaded configs");
+    }
+}

+ 217 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/cmd/StatsCommands.java

@@ -0,0 +1,217 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.cmd;
+
+import com.pro_crafting.mc.commandframework.Command;
+import com.pro_crafting.mc.commandframework.CommandArgs;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.inventory.CustomInv;
+import de.butzlabben.missilewars.inventory.OrcItem;
+import de.butzlabben.missilewars.inventory.pages.PageGUICreator;
+import de.butzlabben.missilewars.util.stats.PlayerGuiFactory;
+import de.butzlabben.missilewars.util.stats.PreFetcher;
+import de.butzlabben.missilewars.util.stats.StatsUtil;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.stats.PlayerStats;
+import de.butzlabben.missilewars.wrapper.stats.SavedStats;
+import de.butzlabben.missilewars.wrapper.stats.StatsFetcher;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.bukkit.Material;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class StatsCommands {
+
+    private final static int MAX_FIGHT_DRAW_PERCENTAGE = 15;
+    private final static int MIN_FIGHT_DURATION = 5;
+    private final static double MAX_AVIATION_WIN = 0.1;
+    private final SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy");
+    private final SimpleDateFormat preciseFormat = new SimpleDateFormat("hh:mm dd.MM.yyyy");
+
+    @Command(name = "mw.stats.recommendations", permission = "mw.stats.recommendations", inGameOnly = true, usage = "/mw stats recommendations [from] [arena]")
+    public void onRecommendations(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        StatsFetcher fetcher = getFetcher(player, args);
+        if (fetcher == null) return;
+        SavedStats avgStatsWithDraws = fetcher.getAverageSavedStats(false);
+        SavedStats avgStatsWithoutDraws = fetcher.getAverageSavedStats(true);
+        List<String> recommendations = new ArrayList<>();
+        int gameCount = fetcher.getGameCount();
+
+        double avgWins = avgStatsWithoutDraws.getTeamWon();
+        if (Math.abs(avgWins - 1) > MAX_AVIATION_WIN) {
+            recommendations.add("It could be, that your map is biased to one team, as wins are not equally distributed");
+        }
+
+        int draws = fetcher.getDrawFights();
+        if ((((double) draws / (double) gameCount) * 100) > MAX_FIGHT_DRAW_PERCENTAGE) {
+            recommendations.add("Increase the game_length option. More than 15% of your games are draws");
+        }
+
+        Duration duration = Duration.ofMillis(avgStatsWithoutDraws.getTimeElapsed());
+        if (((double) duration.getSeconds() / 60.0) <= MIN_FIGHT_DURATION) {
+            recommendations.add("Remove some overpowered features. The average game length at won games is under 5 minutes");
+        }
+        // TODO implement more features
+
+        if (recommendations.size() == 0) {
+            player.sendMessage(MessageConfig.getPrefix() + "§aThere are currently no recommendations, everything seems fine :)");
+        } else {
+            player.sendMessage(MessageConfig.getPrefix() + "§7=====[ §eMissileWars recommendations §7]=====");
+            recommendations.forEach(str -> player.sendMessage(MessageConfig.getPrefix() + str));
+        }
+    }
+
+    @Command(name = "mw.stats", permission = "mw.stats", inGameOnly = true, usage = "/mw stats [from] [arena]")
+    public void onStats(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        StatsFetcher fetcher = getFetcher(player, args);
+        if (fetcher == null) return;
+        String arena = fetcher.getArena().replace("%", "");
+
+        PreFetcher.PrePlayerFetchRunnable preFetchRunnable = PreFetcher.preFetchPlayers(fetcher);
+
+        CustomInv inv = new CustomInv("§eMissileWars statistics", 3);
+        List<String> criteriaLore = Arrays.asList("§7Statistics since: §e" + format.format(fetcher.getFrom()), "§7Specified arena: §e" + (arena.equals("") ? "any" : arena));
+        inv.addItem(4, new OrcItem(Material.FEATHER, "§aStatistics search criteria", criteriaLore));
+
+        int gameCount = fetcher.getGameCount();
+
+        SavedStats avgStatsWithDraws = fetcher.getAverageSavedStats(false);
+        SavedStats avgStatsWithoutDraws = fetcher.getAverageSavedStats(true);
+        int draws = fetcher.getDrawFights();
+        String duration = StatsUtil.formatDuration(Duration.ofMillis(avgStatsWithDraws.getTimeElapsed()));
+
+        List<String> generalLore = Arrays.asList("§7Fights: §e" + gameCount, "§7Average game length: §e" + duration,
+                "§7Games with a draw: §e" + draws,
+                "§7Team1-wins ÷ Team2-wins: §e" + StatsUtil.formatDouble(avgStatsWithoutDraws.getTeamWon()),
+                "§7Average player count: §e" + StatsUtil.formatDouble(avgStatsWithDraws.getPlayerCount()));
+        inv.addItem(9, new OrcItem(Material.SLIME_BLOCK, "§aGeneral statistics", generalLore));
+
+        List<String> playerLore = Arrays.asList("§7Unique players: §e" + fetcher.getUniquePlayers(), "", "§7Click to list players");
+        OrcItem players = new OrcItem(VersionUtil.getPlayerSkullMaterial(), "§aPlayers", playerLore);
+        players.setOnClick((p, inventory, item) -> {
+            p.closeInventory();
+            preFetchRunnable.stop();
+            p.chat("/mw stats players " + format.format(fetcher.getFrom()) + " " + arena);
+        });
+        inv.addItem(13, players);
+
+        List<String> gamesLore = Arrays.asList("", "§7Click to list games");
+        OrcItem games = new OrcItem(Material.PAPER, "§aGames", gamesLore);
+        games.setOnClick((p, inventory, item) -> {
+            p.closeInventory();
+            p.chat("/mw stats list " + format.format(fetcher.getFrom()) + " " + arena);
+        });
+        inv.addItem(17, games);
+
+        inv.prettyFill();
+        player.openInventory(inv.getInventory(player));
+    }
+
+    @Command(name = "mw.stats.players", permission = "mw.stats.players", inGameOnly = true, usage = "/mw stats players [from] [arena]")
+    public void onPlayers(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        StatsFetcher fetcher = getFetcher(player, args);
+        if (fetcher == null) return;
+        List<UUID> players = fetcher.getPlayers();
+        List<PlayerStats> playerStats = players.stream().map(fetcher::getStatsFrom).collect(Collectors.toList());
+
+        PlayerGuiFactory playerGuiFactory = new PlayerGuiFactory(playerStats);
+        playerGuiFactory.openWhenReady(player);
+    }
+
+    @Command(name = "mw.stats.list", permission = "mw.stats.list", inGameOnly = true, usage = "/mw stats list [from] [arena]")
+    public void onList(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        StatsFetcher fetcher = getFetcher(player, args);
+        if (fetcher == null) return;
+        List<SavedStats> players = fetcher.getAllStats();
+
+        PageGUICreator<SavedStats> creator = new PageGUICreator<>("§eGame list", players, (item) -> {
+            Duration duration = Duration.ofMillis(item.getTimeElapsed());
+            return new OrcItem(Material.TNT, "§7" + players.indexOf(item),
+                    "§7Started: §e" + preciseFormat.format(item.getTimeStart()),
+                    "§7Duration: §e" + StatsUtil.formatDuration(duration), "§7Arena: §e" + item.getArena(),
+                    "§7Players: §e" + (int) item.getPlayerCount(), "§7Team won: §e" + (int) item.getTeamWon());
+        });
+        creator.show(player);
+    }
+
+    private StatsFetcher getFetcher(Player player, CommandArgs args) {
+        if (!Config.isFightStatsEnabled()) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cFightStats are not enabled!");
+            return null;
+        }
+        Date from = new Date(0);
+        String arena = "";
+        if (args.length() > 0) {
+            try {
+                from = format.parse(args.getArgs(0));
+            } catch (ParseException e) {
+                player.sendMessage(MessageConfig.getPrefix() + "§cPlease use the date format dd.MM.yyyy");
+                return null;
+            }
+            if (args.length() > 1) {
+                arena = args.getArgs(1);
+            }
+        }
+
+        StatsFetcher fetcher = new StatsFetcher(from, arena);
+        if (fetcher.getGameCount() < 10) {
+            player.sendMessage(MessageConfig.getPrefix() + "Please play more than 10 games to enable fight stats");
+            return null;
+        }
+        player.sendMessage(MessageConfig.getPrefix() + "Loading data...");
+        return fetcher;
+    }
+}

+ 164 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/cmd/UserCommands.java

@@ -0,0 +1,164 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.cmd;
+
+import com.pro_crafting.mc.commandframework.Command;
+import com.pro_crafting.mc.commandframework.CommandArgs;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.game.GameState;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import de.butzlabben.missilewars.wrapper.abstracts.MapChooseProcedure;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import java.util.Optional;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class UserCommands {
+
+    @Command(name = "mw.change", usage = "/mw change <1|2>", permission = "mw.change", description = "Changes your team", inGameOnly = true)
+    public void changeCommand(CommandArgs args) {
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+        if (game == null) {
+            player.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+
+        if (game.getState() != GameState.LOBBY) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cThe game is not in the right state to change your team right now");
+            return;
+        }
+
+        if (args.length() != 1) {
+            player.sendMessage(MessageConfig.getPrefix() + "§c/mw vote <arena>");
+            return;
+        }
+
+        if (args.length() != 1) {
+            player.sendMessage(MessageConfig.getPrefix() + "§c/mw change <1|2>");
+            return;
+        }
+        try {
+            MWPlayer mwPlayer = game.getPlayer(player);
+            int teamNumber = Integer.parseInt(args.getArgs(0));
+            Team to = teamNumber == 1 ? game.getTeam1() : game.getTeam2();
+            int otherCount = to.getEnemyTeam().getMembers().size() - 1;
+            int toCount = to.getMembers().size() + 1;
+            int diff = toCount - otherCount;
+            if (diff > 1) {
+                player.sendMessage(MessageConfig.getMessage("cannot_change_difference"));
+                return;
+            }
+            mwPlayer.getTeam().removeMember(mwPlayer);
+            to.addMember(mwPlayer);
+
+            player.sendMessage(MessageConfig.getMessage("team_changed").replace("%team%", to.getFullname()));
+        } catch (NumberFormatException exception) {
+            player.sendMessage(MessageConfig.getPrefix() + "§c/mw change <1|2>");
+        }
+    }
+
+
+    @Command(name = "mw.vote", usage = "/mw vote <arena>", description = "Stops the game", inGameOnly = true)
+    public void voteCommand(CommandArgs args) {
+        // TODO more messageconfig
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+        if (game == null) {
+            player.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+
+        if (game.getState() != GameState.LOBBY) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cThe game is not in the right state to vote right now");
+            return;
+        }
+
+        if (game.getLobby().getMapChooseProcedure() != MapChooseProcedure.MAPVOTING) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cYou can't vote in this game");
+            return;
+        }
+
+        if (game.getArena() != null) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cA map was already elected");
+            return;
+        }
+
+        if (args.length() != 1) {
+            player.sendMessage(MessageConfig.getPrefix() + "§c/mw vote <arena>");
+            return;
+        }
+
+        String arenaName = args.getArgs(0);
+        Optional<Arena> arena = Arenas.getFromName(arenaName);
+        if (!game.getVotes().containsKey(arenaName) || !arena.isPresent()) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cNo map with this title was found");
+            return;
+        }
+
+        game.getVotes().put(arenaName, game.getVotes().get(arenaName) + 1);
+        player.sendMessage(MessageConfig.getMessage("vote.success").replace("%map%", arena.get().getDisplayName()));
+    }
+
+    @Command(name = "mw.quit", inGameOnly = true, usage = "/mw quit", permission = "mw.quit", description = "Quit a game")
+    public void onQuit(CommandArgs args) {
+        // TODO message config
+        CommandSender sender = args.getSender();
+        if (!(sender instanceof Player)) {
+            sender.sendMessage(MessageConfig.getPrefix() + "§cYou are not a player");
+            return;
+        }
+
+        Player player = (Player) sender;
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+        if (game == null) {
+            player.sendMessage(MessageConfig.getMessage("not_in_arena"));
+            return;
+        }
+        MWPlayer mwPlayer = game.getPlayer(player);
+        if (mwPlayer == null) {
+            player.sendMessage(MessageConfig.getPrefix() + "§cYou are not a member in this arena. Something went wrong pretty badly :(");
+            return;
+        }
+        Location endSpawn = game.getLobby().getAfterGameSpawn();
+        if (GameManager.getInstance().getGame(endSpawn) != null) {
+            endSpawn = Config.getFallbackSpawn();
+        }
+        player.teleport(endSpawn);
+        player.sendMessage(MessageConfig.getMessage("game_quit"));
+    }
+}

+ 80 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Arenas.java

@@ -0,0 +1,80 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.util.SetupUtil;
+import de.butzlabben.missilewars.util.serialization.Serializer;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+
+public class Arenas {
+
+    @Getter private static final List<Arena> arenas = new ArrayList<>();
+
+    public static void load() {
+        arenas.clear();
+
+        File folder = new File(Config.getArenaFolder());
+        File[] files = folder.listFiles();
+        if (files.length == 0) {
+            File defaultArena = new File(folder, "arena.yml");
+            try {
+                defaultArena.createNewFile();
+                Serializer.serialize(defaultArena, new Arena());
+            } catch (IOException exception) {
+                Logger.ERROR.log("Could not create default arena config");
+                Logger.ERROR.log("As there are no arenas present, the plugin is shutting down");
+                exception.printStackTrace();
+                Bukkit.getPluginManager().disablePlugin(MissileWars.getInstance());
+                return;
+            }
+            files = new File[] {defaultArena};
+        }
+        for (File config : files) {
+            if (!config.getName().endsWith(".yml") && !config.getName().endsWith(".yaml")) continue;
+            try {
+                Arena arena = Serializer.deserialize(config, Arena.class);
+                if (getFromName(arena.getName()).isPresent()) {
+                    Logger.WARN.log("There are several arenas configured with the name \"" + arena.getName() + "\". Arenas must have a unique name");
+                    continue;
+                }
+                SetupUtil.checkMap(arena.getTemplateWorld());
+                // Save for possible new values
+                Serializer.serialize(config, arena);
+                arenas.add(arena);
+            } catch (IOException exception) {
+                Logger.ERROR.log("Could not load config for arena " + config.getName());
+                exception.printStackTrace();
+            }
+        }
+    }
+
+    public static Optional<Arena> getFromName(String name) {
+        return arenas.stream().filter(arena -> arena.getName().equalsIgnoreCase(name)).findFirst();
+    }
+}

+ 466 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Game.java

@@ -0,0 +1,466 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+import com.google.common.base.Preconditions;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.timer.EndTimer;
+import de.butzlabben.missilewars.game.timer.GameTimer;
+import de.butzlabben.missilewars.game.timer.LobbyTimer;
+import de.butzlabben.missilewars.game.timer.Timer;
+import de.butzlabben.missilewars.listener.EndListener;
+import de.butzlabben.missilewars.listener.GameBoundListener;
+import de.butzlabben.missilewars.listener.GameListener;
+import de.butzlabben.missilewars.listener.LobbyListener;
+import de.butzlabben.missilewars.util.MoneyUtil;
+import de.butzlabben.missilewars.util.MotdManager;
+import de.butzlabben.missilewars.util.ScoreboardManager;
+import de.butzlabben.missilewars.util.serialization.Serializer;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import de.butzlabben.missilewars.wrapper.abstracts.GameWorld;
+import de.butzlabben.missilewars.wrapper.abstracts.Lobby;
+import de.butzlabben.missilewars.wrapper.abstracts.MapChooseProcedure;
+import de.butzlabben.missilewars.wrapper.event.GameEndEvent;
+import de.butzlabben.missilewars.wrapper.event.GameStartEvent;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaJoinEvent;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import de.butzlabben.missilewars.wrapper.stats.FightStats;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Consumer;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.scheduler.BukkitTask;
+import org.bukkit.scoreboard.Scoreboard;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+
+@Getter
+@ToString(of = {"gameWorld", "players", "lobby", "arena", "team1", "team2", "state"})
+public class Game {
+
+    private static final Map<String, Integer> cycles = new HashMap<>();
+    private static int fights = 0;
+    private final Map<UUID, MWPlayer> players = new HashMap<>();
+    private final Map<String, Integer> votes = new HashMap<>(); // Votes for the maps.
+    @Getter private final Lobby lobby;
+    private final HashMap<UUID, BukkitTask> playerTasks = new HashMap<>();
+    private Timer timer;
+    private BukkitTask bt;
+    private GameState state = GameState.LOBBY;
+    private Team team1;
+    private Team team2;
+    @Setter private boolean draw = true;
+    @Getter private boolean ready = false;
+    private boolean restart = false;
+    private GameWorld gameWorld;
+    private long timestart;
+    @Getter private Arena arena;
+    private Scoreboard scoreboard;
+    private ScoreboardManager scoreboardManager;
+    private GameBoundListener listener;
+
+    public Game(Lobby lobby) {
+        Logger.BOOT.log("Loading game " + lobby.getDisplayName());
+        this.lobby = lobby;
+        if (lobby.getBukkitWorld() == null) {
+            Logger.ERROR.log("Lobby world in arena \"" + lobby.getName() + "\" must not be null");
+            return;
+        }
+
+        try {
+            Serializer.setWorldAtAllLocations(lobby, lobby.getBukkitWorld());
+        } catch (Exception exception) {
+            Logger.ERROR.log("Could not inject world object at lobby " + lobby.getName());
+            exception.printStackTrace();
+            return;
+        }
+
+        if (lobby.getPossibleArenas().size() == 0) {
+            Logger.ERROR.log(("At least one valid arena must be set at lobby " + lobby.getName()));
+            return;
+        }
+
+        if (lobby.getPossibleArenas().stream().noneMatch(a -> Arenas.getFromName(a).isPresent())) {
+            Logger.ERROR.log(("None of the specified arenas match a real arena for the lobby " + lobby.getName()));
+            return;
+        }
+
+        gameWorld = new GameWorld(this, "");
+
+        players.clear();
+
+        GameBoundListener listener = new LobbyListener(this);
+        Bukkit.getPluginManager().registerEvents(listener, MissileWars.getInstance());
+        this.listener = listener;
+
+        team1 = new Team(lobby.getTeam1Name(), lobby.getTeam1Color(), this);
+        team2 = new Team(lobby.getTeam2Name(), lobby.getTeam2Color(), this);
+
+        scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
+
+        org.bukkit.scoreboard.Team t = scoreboard.getTeam("0" + team1.getFullname());
+        if (t != null)
+            t.unregister();
+        t = scoreboard.registerNewTeam("0" + team1.getFullname());
+        t.setPrefix(team1.getColorCode());
+        VersionUtil.setScoreboardTeamColor(t, ChatColor.getByChar(team1.getColorCode().charAt(1)));
+        team1.setSBTeam(t);
+
+        t = scoreboard.getTeam("1" + team2.getFullname());
+        if (t != null)
+            t.unregister();
+        t = scoreboard.registerNewTeam("1" + team2.getFullname());
+        t.setPrefix(team2.getColorCode());
+        VersionUtil.setScoreboardTeamColor(t, ChatColor.getByChar(team2.getColorCode().charAt(1)));
+        team2.setSBTeam(t);
+
+        t = scoreboard.getTeam("2Guest§7");
+        if (t != null)
+            t.unregister();
+        t = scoreboard.registerNewTeam("2Guest§7");
+        t.setPrefix("§7");
+
+        VersionUtil.setScoreboardTeamColor(t, ChatColor.GRAY);
+
+        scoreboardManager = new ScoreboardManager(this, scoreboard);
+
+        Logger.DEBUG.log("Registering, teleporting, etc. all players");
+
+        for (Player all : Bukkit.getOnlinePlayers()) {
+            if (!isIn(all.getLocation()))
+                continue;
+            Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(),
+                    () -> Bukkit.getPluginManager().callEvent(new PlayerArenaJoinEvent(all, this)), 2);
+        }
+
+        // Change MOTD
+        if (!Config.isMultipleLobbies()) MotdManager.getInstance().updateMOTD(this);
+
+        Logger.DEBUG.log("Start timer");
+        stopTimer();
+        timer = new LobbyTimer(this, lobby.getLobbyTime());
+        bt = Bukkit.getScheduler().runTaskTimer(MissileWars.getInstance(), timer, 0, 20);
+
+        if (Config.isSetup()) {
+            Logger.WARN.log("Did not fully initialize lobby " + lobby.getName() + " as the plugin is in setup mode");
+            return;
+        }
+
+        if (lobby.getMapChooseProcedure() == MapChooseProcedure.FIRST) {
+            setArena(lobby.getArenas().get(0));
+        } else if (lobby.getMapChooseProcedure() == MapChooseProcedure.MAPCYCLE) {
+            final int lastMapIndex = cycles.getOrDefault(lobby.getName(), -1);
+            List<Arena> arenas = lobby.getArenas();
+            int index = lastMapIndex >= arenas.size() - 1 ? 0 : lastMapIndex + 1;
+            cycles.put(lobby.getName(), index);
+            setArena(arenas.get(index));
+        } else if (lobby.getMapChooseProcedure() == MapChooseProcedure.MAPVOTING) {
+            if (lobby.getArenas().size() == 1) {
+                setArena(lobby.getArenas().get(0));
+            }
+            lobby.getArenas().forEach(arena -> votes.put(arena.getName(), 0));
+        }
+
+
+        Logger.DEBUG.log("Making game ready");
+        ++fights;
+        if (fights >= Config.getFightRestart())
+            restart = true;
+
+        FightStats.checkTables();
+        Logger.DEBUG.log("Fights: " + fights);
+    }
+
+    public void startGame() {
+        if (Config.isSetup()) {
+            Logger.WARN.log("Did not start game. Setup mode is still enabled");
+            return;
+        }
+
+        World world = gameWorld.getWorld();
+
+        if (world == null) {
+            Logger.ERROR.log("Could not start game in arena \"" + arena.getName() + "\". World is null");
+            return;
+        }
+
+        stopTimer();
+        timer = new GameTimer(this);
+        bt = Bukkit.getScheduler().runTaskTimer(MissileWars.getInstance(), timer, 5, 20);
+
+        HandlerList.unregisterAll(listener);
+
+        GameBoundListener listener = new GameListener(this);
+        Bukkit.getPluginManager().registerEvents(listener, MissileWars.getInstance());
+        this.listener = listener;
+
+        state = GameState.INGAME;
+        timestart = System.currentTimeMillis();
+
+        applyForAllPlayers(this::startForPlayer);
+
+        // Set intervals
+        team1.updateIntervals(arena.getInterval(team1.getMembers().size()));
+        team2.updateIntervals(arena.getInterval(team2.getMembers().size()));
+
+        // Change MOTD
+        if (!Config.isMultipleLobbies())
+            MotdManager.getInstance().updateMOTD(this);
+
+        Bukkit.getPluginManager().callEvent(new GameStartEvent(this));
+    }
+
+    private void stopTimer() {
+        if (bt != null)
+            bt.cancel();
+    }
+
+    public void stopGame() {
+        if (Config.isSetup())
+            return;
+
+        Logger.DEBUG.log("Stopping");
+        state = GameState.END;
+        for (BukkitTask bt : playerTasks.values()) {
+            bt.cancel();
+        }
+
+        Logger.DEBUG.log("Stopping for players");
+        int money = arena.getMoney().getDraw();
+        for (Player all : Bukkit.getOnlinePlayers()) {
+            if (!isIn(all.getLocation()))
+                continue;
+            Logger.DEBUG.log("Stopping for: " + all.getName());
+
+            all.setGameMode(GameMode.SPECTATOR);
+            all.teleport(arena.getSpectatorSpawn());
+            all.setHealth(all.getMaxHealth());
+
+            VersionUtil.playDraw(all);
+            if (draw) {
+                if (getPlayer(all).getTeam() == null)
+                    continue;
+                MoneyUtil.giveMoney(all.getUniqueId(), money);
+            }
+        }
+
+        stopTimer();
+
+        HandlerList.unregisterAll(listener);
+        GameBoundListener listener = new EndListener(this);
+        try {
+            Bukkit.getPluginManager().registerEvents(listener, MissileWars.getInstance());
+        } catch (Exception ignored) {
+        }
+        this.listener = listener;
+
+        timer = new EndTimer(this);
+        bt = Bukkit.getScheduler().runTaskTimer(MissileWars.getInstance(), timer, 5, 20);
+        scoreboardManager.removeScoreboard();
+
+        // Change MOTD
+        if (!Config.isMultipleLobbies())
+            MotdManager.getInstance().updateMOTD(this);
+
+        if (getArena().isSaveStatistics()) {
+            FightStats stats = new FightStats(this);
+            stats.insert();
+        }
+
+        if (draw) Bukkit.getPluginManager().callEvent(new GameEndEvent(this, null));
+
+        Logger.DEBUG.log("Stopped completely");
+    }
+
+    public void reset() {
+        if (Config.isSetup())
+            return;
+
+        if (restart) {
+            Bukkit.getServer().spigot().restart();
+            return;
+        }
+
+        GameManager.getInstance().restartGame(lobby);
+    }
+
+    public void appendRestart() {
+        restart = true;
+    }
+
+    public void draw(boolean draw) {
+        this.draw = draw;
+    }
+
+    public void disable() {
+        if (state == GameState.INGAME) stopGame();
+
+        HandlerList.unregisterAll(listener);
+
+        stopTimer();
+
+        applyForAllPlayers(player -> player.teleport(lobby.getAfterGameSpawn()));
+        if (gameWorld.getWorldName() != null) {
+            gameWorld.sendPlayersBack();
+            gameWorld.unload();
+            gameWorld.delete();
+        }
+        scoreboardManager.removeScoreboard();
+        team1 = null;
+        team2 = null;
+    }
+
+    public boolean isInLobbyArea(Location location) {
+        World world = location.getWorld();
+        if (world == null) return false;
+        if (world.getName().equals(lobby.getWorld())) return lobby.getArea().isInArea(location);
+        return false;
+    }
+
+
+    public boolean isInGameArea(Location location) {
+        if (isInGameWorld(location)) return arena.getGameArea().isInArea(location);
+        return false;
+    }
+
+    public boolean isInGameWorld(Location location) {
+        World world = location.getWorld();
+        return gameWorld.isWorld(world);
+    }
+
+    public boolean isIn(Location location) {
+        return isInLobbyArea(location) || isInGameWorld(location);
+    }
+
+    public MWPlayer getPlayer(Player player) {
+        return players.get(player.getUniqueId());
+    }
+
+    public void broadcast(String message) {
+        for (MWPlayer player : players.values()) {
+            Player p = player.getPlayer();
+            if (p != null && p.isOnline()) p.sendMessage(message);
+        }
+    }
+
+    public void startForPlayer(Player player) {
+        MWPlayer mwPlayer = getPlayer(player);
+        if (mwPlayer == null) {
+            System.err.println("[MissileWars] Error starting game at player " + player.getName());
+            return;
+        }
+
+        player.teleport(mwPlayer.getTeam().getSpawn());
+        ItemStack air = new ItemStack(Material.AIR);
+        ItemStack bow = new ItemStack(Material.BOW);
+        bow.addEnchantment(Enchantment.ARROW_FIRE, 1);
+        bow.addEnchantment(Enchantment.ARROW_DAMAGE, 1);
+        bow.addEnchantment(Enchantment.ARROW_KNOCKBACK, 1);
+        ItemMeta im = bow.getItemMeta();
+        im.addEnchant(Enchantment.DAMAGE_ALL, 6, true);
+        bow.setItemMeta(im);
+        VersionUtil.setUnbreakable(bow);
+
+        player.getInventory().setItem(0, air);
+        player.getInventory().setItem(8, air);
+        player.getInventory().addItem(bow);
+        mwPlayer.getTeam().setTeamArmor(player);
+        player.setGameMode(GameMode.SURVIVAL);
+        player.setLevel(0);
+        player.setFireTicks(0);
+        playerTasks.put(player.getUniqueId(),
+                Bukkit.getScheduler().runTaskTimer(MissileWars.getInstance(), mwPlayer, 0, 20));
+
+    }
+
+    public void setArena(Arena arena) {
+        Preconditions.checkNotNull(arena);
+        if (this.arena != null) {
+            throw new IllegalStateException("Arena already set");
+        }
+
+        arena.getMissileConfiguration().check();
+        if (arena.getMissileConfiguration().getMissiles().size() == 0) {
+            throw new IllegalStateException("The game cannot be started, when 0 missiles are configured");
+        }
+
+        this.arena = arena.toBuilder().build();
+        this.arena.setSpectatorSpawn(arena.getSpectatorSpawn().clone());
+        this.arena.setTeam1Spawn(arena.getTeam1Spawn().clone());
+        this.arena.setTeam2Spawn(arena.getTeam2Spawn().clone());
+
+        // Load world
+        this.gameWorld = new GameWorld(this, this.arena.getTemplateWorld());
+        this.gameWorld.load();
+
+        try {
+            Serializer.setWorldAtAllLocations(this.arena, gameWorld.getWorld());
+            team1.setSpawn(this.arena.getTeam1Spawn());
+            team2.setSpawn(this.arena.getTeam2Spawn());
+        } catch (Exception exception) {
+            Logger.ERROR.log("Could not inject world object at arena " + this.arena.getName());
+            exception.printStackTrace();
+            return;
+        }
+
+        if (lobby.getMapChooseProcedure() == MapChooseProcedure.MAPVOTING) {
+            this.broadcast(MessageConfig.getMessage("vote.finished").replace("%map%", this.arena.getDisplayName()));
+        }
+        applyForAllPlayers(p -> p.getInventory().setItem(4, new ItemStack(Material.AIR)));
+
+        ready = true;
+    }
+
+    public void applyForAllPlayers(Consumer<Player> consumer) {
+        for (Player player : Bukkit.getOnlinePlayers()) {
+            if (!isIn(player.getLocation())) continue;
+            consumer.accept(player);
+        }
+    }
+
+    public MWPlayer addPlayer(Player player) {
+        if (players.containsKey(player.getUniqueId())) return players.get(player.getUniqueId());
+        MWPlayer mwPlayer = new MWPlayer(player, this);
+        players.put(player.getUniqueId(), mwPlayer);
+        return mwPlayer;
+    }
+}

+ 144 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameManager.java

@@ -0,0 +1,144 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+import com.google.common.base.Preconditions;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.util.serialization.Serializer;
+import de.butzlabben.missilewars.wrapper.abstracts.Lobby;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+
+@Getter
+public class GameManager {
+
+    @Getter
+    private static final GameManager instance = new GameManager();
+    private final HashMap<String, Game> games = new HashMap<>();
+
+
+    public void disableAll() {
+        games.values().forEach(Game::disable);
+        games.clear();
+    }
+
+    public void loadGames() {
+        File[] files = null;
+        if (Config.isMultipleLobbies()) {
+            files = new File(Config.getLobbiesFolder()).listFiles();
+        } else {
+            File file = new File(Config.getLobbiesFolder() + "/" + Config.getDefaultLobby());
+            if (file.exists()) {
+                files = new File[] {file};
+            }
+        }
+        if (files == null) files = new File[0];
+
+        if (files.length == 0) {
+            Logger.WARN.log("No lobby configs found. Creating default one");
+            File file = new File(Config.getLobbiesFolder() + "/" + Config.getDefaultLobby());
+            try {
+                file.createNewFile();
+                Serializer.serialize(file, new Lobby());
+            } catch (IOException exception) {
+                Logger.ERROR.log("Could not create default arena config");
+                Logger.ERROR.log("As there are no arenas present, the plugin is shutting down");
+                exception.printStackTrace();
+                Bukkit.getPluginManager().disablePlugin(MissileWars.getInstance());
+                return;
+            }
+            files = new File[] {file};
+        }
+
+        loadGames(files);
+    }
+
+    private void loadGames(File[] files) {
+        disableAll();
+
+        for (File game : files) {
+            if (game == null)
+                continue;
+            if (!game.getName().endsWith(".yml") && !game.getName().endsWith(".yaml")) continue;
+
+            Logger.BOOT.log("Loading lobby " + game.getName());
+            try {
+                Lobby lobby = Serializer.deserialize(game, Lobby.class);
+                if (lobby == null) {
+                    Logger.ERROR.log("Could not load lobby " + game.getName());
+                    continue;
+                }
+                Game potentialOtherGame = getGame(lobby.getName());
+                if (potentialOtherGame != null) {
+                    Logger.ERROR.log("A lobby with the same name was already loaded. Names of lobbies must be unique, this lobby will not be loaded");
+                    continue;
+                }
+                lobby.setFile(game);
+                restartGame(lobby);
+                Logger.BOOTDONE.log("Loaded lobby " + game.getName());
+            } catch (IOException exception) {
+                Logger.ERROR.log("Could not load lobby " + game.getName());
+                exception.printStackTrace();
+            }
+        }
+    }
+
+    public void disableGame(String name) {
+        Preconditions.checkNotNull(name);
+        Game game = getGame(name);
+        if (game == null)
+            return;
+        game.disable();
+        games.remove(name);
+    }
+
+    public void restartGame(Lobby oldLobby) {
+        String name = oldLobby.getName();
+        disableGame(name);
+        try {
+            Lobby lobby = Serializer.deserialize(oldLobby.getFile(), Lobby.class);
+            lobby.setFile(oldLobby.getFile());
+            // Save for possible new values
+            Serializer.serialize(oldLobby.getFile(), lobby);
+            games.put(name, new Game(lobby));
+        } catch (IOException exception) {
+            Logger.ERROR.log("Could not load lobby " + oldLobby.getName());
+            exception.printStackTrace();
+        }
+    }
+
+    public Game getGame(String name) {
+        return games.get(name);
+    }
+
+    public Game getGame(Location location) {
+        for (Game game : GameManager.getInstance().getGames().values()) {
+            if (game.isIn(location)) {
+                return game;
+            }
+        }
+        return null;
+    }
+}

+ 30 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameState.java

@@ -0,0 +1,30 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+public enum GameState {
+
+    LOBBY,
+    INGAME,
+    END
+}

+ 44 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/EndTimer.java

@@ -0,0 +1,44 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game.timer;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.game.Game;
+
+/**
+ * @author Butzlabben
+ * @since 14.01.2018
+ */
+public class EndTimer extends Timer {
+
+    public EndTimer(Game game) {
+        super(game);
+        seconds = 21;
+    }
+
+    @Override
+    public void tick() {
+        if (seconds == 20)
+            broadcast(MessageConfig.getMessage("game_starts_new_in").replace("%seconds%", "" + seconds));
+        else if (seconds == 0)
+            getGame().reset();
+        --seconds;
+    }
+
+}

+ 76 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/GameTimer.java

@@ -0,0 +1,76 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game.timer;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.game.Game;
+
+/**
+ * @author Butzlabben
+ * @since 06.01.2018
+ */
+public class GameTimer extends Timer {
+
+    public GameTimer(Game game) {
+        super(game);
+        seconds = game.getArena().getGameDuration() * 60;
+    }
+
+    @Override
+    public void tick() {
+        if (seconds == 7200) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "120"));
+        } else if (seconds == 5400) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "90"));
+        } else if (seconds == 3600) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "60"));
+        } else if (seconds == 1800) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "30"));
+        } else if (seconds == 1200) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "20"));
+        } else if (seconds == 600) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "10"));
+        } else if (seconds == 300) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "5"));
+        } else if (seconds == 180) {
+            broadcast(MessageConfig.getMessage("game_ends_in_minutes").replace("%minutes%", "3"));
+        } else if (seconds == 60) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "60"));
+        } else if (seconds == 30) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "30"));
+        } else if (seconds == 10) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "10"));
+        } else if (seconds == 5) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "5"));
+        } else if (seconds == 4) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "4"));
+        } else if (seconds == 3) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "3"));
+        } else if (seconds == 2) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "2"));
+        } else if (seconds == 1) {
+            broadcast(MessageConfig.getMessage("game_ends_in_seconds").replace("%seconds%", "1"));
+        } else if (seconds == 0) {
+            getGame().stopGame();
+        }
+        if (seconds % 10 == 0)
+            getGame().getScoreboardManager().updateInGameScoreboard();
+        --seconds;
+    }
+}

+ 142 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/LobbyTimer.java

@@ -0,0 +1,142 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game.timer;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import de.butzlabben.missilewars.wrapper.abstracts.MapChooseProcedure;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author Butzlabben
+ * @since 11.01.2018
+ */
+public class LobbyTimer extends Timer implements Runnable {
+
+    private final int startTime;
+    private int remaining = 90; // for sending messages
+
+
+    public LobbyTimer(Game game, int startTime) {
+        super(game);
+        this.startTime = startTime;
+        seconds = startTime;
+    }
+
+    @Override
+    public void tick() {
+        if (getGame().getPlayers().values().size() == 0)
+            return;
+
+        for (MWPlayer mp : getGame().getPlayers().values()) {
+            if (mp.getPlayer() != null) mp.getPlayer().setLevel(seconds);
+            else {
+                if (mp.getTeam() != null) mp.getTeam().removeMember(mp);
+                getGame().getPlayers().remove(mp.getUUID());
+            }
+        }
+
+        int size1 = getGame().getTeam1().getMembers().size();
+        int size2 = getGame().getTeam2().getMembers().size();
+        if (size1 == 0 || size2 == 0) {
+            seconds = startTime;
+            return;
+        }
+        --remaining;
+        if (remaining == 0) {
+            if (size1 + size1 < getGame().getLobby().getMinSize())
+                return;
+            seconds = startTime;
+            remaining = 90;
+            broadcast(MessageConfig.getMessage("not_enough_players"));
+        }
+        if (seconds == 120) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "120"));
+            playPling();
+        } else if (seconds == 60) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "60"));
+            playPling();
+        } else if (seconds == 30) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "30"));
+            playPling();
+        } else if (seconds == 10) {
+            checkVote();
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "10"));
+            playPling();
+        } else if (seconds == 5) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "5"));
+            playPling();
+        } else if (seconds == 4) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "4"));
+            playPling();
+        } else if (seconds == 3) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "3"));
+            playPling();
+        } else if (seconds == 2) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "2"));
+            playPling();
+        } else if (seconds == 1) {
+            broadcast(MessageConfig.getMessage("game_starts_in").replace("%seconds%", "1"));
+            playPling();
+
+        } else if (seconds == 0) {
+            int diff = size1 - size2;
+            if (diff >= 2 || diff <= -2) {
+                broadcast(MessageConfig.getMessage("teams_unequal"));
+                seconds = startTime;
+                return;
+            }
+            broadcast(MessageConfig.getMessage("game_starts"));
+            playPling();
+            getGame().startGame();
+            return;
+        }
+        --seconds;
+    }
+
+    private void playPling() {
+        for (MWPlayer p : getGame().getPlayers().values()) {
+            VersionUtil.playPling(p.getPlayer());
+        }
+    }
+
+    private void checkVote() {
+        Game game = getGame();
+        if (game.getLobby().getMapChooseProcedure() != MapChooseProcedure.MAPVOTING) return;
+        if (game.getArena() != null) return;
+
+        Map.Entry<String, Integer> mostVotes = null;
+        for (Map.Entry<String, Integer> arena : game.getVotes().entrySet()) {
+            if (mostVotes == null) {
+                mostVotes = arena;
+                continue;
+            }
+            if (arena.getValue() > mostVotes.getValue()) mostVotes = arena;
+        }
+        if (mostVotes == null) throw new IllegalStateException("Most votes object was null");
+        Optional<Arena> arena = Arenas.getFromName(mostVotes.getKey());
+        if (!arena.isPresent()) throw new IllegalStateException("Voted arena is not present");
+        game.setArena(arena.get());
+    }
+}

+ 59 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/Timer.java

@@ -0,0 +1,59 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game.timer;
+
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Sound;
+
+/**
+ * @author Butzlabben
+ * @since 14.01.2018
+ */
+
+@RequiredArgsConstructor
+@Getter
+public abstract class Timer implements Runnable {
+
+    private final Game game;
+    public int seconds;
+
+    public int getSeconds() {
+        return seconds;
+    }
+
+    @Override
+    public void run() {
+        tick();
+    }
+
+    public abstract void tick();
+
+    protected void playSoundatAll(Sound s, float pitch) {
+        for (MWPlayer all : game.getPlayers().values()) {
+            all.getPlayer().playSound(all.getPlayer().getLocation(), s, 100, pitch);
+        }
+    }
+
+    protected void broadcast(String message) {
+        game.broadcast(message);
+    }
+}

+ 34 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/CustomInv.java

@@ -0,0 +1,34 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+/**
+ * @author Butzlabben
+ * @since 28.06.2018
+ */
+public class CustomInv extends OrcInventory {
+
+    public CustomInv(String title, int rows) {
+        super(title, rows);
+    }
+
+    public CustomInv(String title, int rows, boolean fill) {
+        super(title, rows, fill);
+    }
+}

+ 28 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/DependListener.java

@@ -0,0 +1,28 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public interface DependListener {
+
+    ItemStack getItemStack(Player p);
+
+}

+ 27 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcClickListener.java

@@ -0,0 +1,27 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+import org.bukkit.entity.Player;
+
+public interface OrcClickListener {
+
+    void onClick(Player p, OrcInventory inv, OrcItem item);
+
+}

+ 167 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcInventory.java

@@ -0,0 +1,167 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.Objects;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.Inventory;
+
+@Getter
+public abstract class OrcInventory {
+
+    protected String title;
+    protected HashMap<Integer, OrcItem> items = new HashMap<>();
+    private int rows;
+    private InventoryType type;
+    private boolean fill = false;
+
+    public OrcInventory(String title) {
+        Objects.requireNonNull(title, "title cannot be null");
+        this.title = title;
+    }
+
+    public OrcInventory(String title, int rows) {
+        this(title);
+        if (rows <= 0 || rows > 6)
+            throw new IllegalArgumentException("rows cannot be smaller than 1 or bigger than 6");
+        this.rows = rows;
+    }
+
+    public OrcInventory(String title, int rows, boolean fill) {
+        this(title, rows);
+        this.fill = fill;
+        if (this.fill) {
+            for (int i = 0; i < rows * 9; i++) {
+                items.put(i, OrcItem.fill);
+            }
+        }
+    }
+
+    public OrcInventory(String title, InventoryType type) {
+        this(title);
+        if (type == null || type == InventoryType.CHEST) {
+            this.type = null;
+            rows = 3;
+        } else {
+            this.type = type;
+        }
+    }
+
+    public void addItem(int slot, OrcItem item) {
+        if (item == null) {
+            removeItem(slot);
+        } else {
+            items.put(slot, item);
+        }
+    }
+
+    public void addItem(int row, int col, OrcItem item) {
+        addItem(row * 9 + col, item);
+    }
+
+    public void removeItem(int slot) {
+        items.remove(slot);
+    }
+
+    public void removeItem(int row, int col) {
+        removeItem(row * 9 + col);
+    }
+
+    public Inventory getInventory(Player p) {
+        return getInventory(p, title);
+    }
+
+    public void redraw(Player p) {
+        p.closeInventory();
+        p.openInventory(getInventory(p));
+    }
+
+    public Inventory getInventory(Player p, String title) {
+        Inventory inv;
+        int size;
+        if (type == null) {
+            inv = Bukkit.createInventory(null, rows * 9, title);
+            size = rows * 9;
+        } else {
+            inv = Bukkit.createInventory(null, type, title);
+            size = type.getDefaultSize();
+        }
+
+        for (Entry<Integer, OrcItem> entry : items.entrySet()) {
+            if (entry.getKey() >= 0 && entry.getKey() < size) {
+                inv.setItem(entry.getKey(), entry.getValue().getItemStack(p));
+            } else {
+                System.err.println("There is a problem with a configured Item!");
+            }
+        }
+
+        OrcListener.getInstance().register(p.getUniqueId(), this);
+
+        return inv;
+    }
+
+    public Inventory getInventory() {
+        Inventory inv;
+        int size;
+        if (type == null) {
+            inv = Bukkit.createInventory(null, rows * 9, title);
+            size = rows * 9;
+        } else {
+            inv = Bukkit.createInventory(null, type, title);
+            size = type.getDefaultSize();
+        }
+
+        for (Entry<Integer, OrcItem> entry : items.entrySet()) {
+            if (entry.getKey() >= 0 && entry.getKey() < size) {
+                inv.setItem(entry.getKey(), entry.getValue().getItemStack());
+            } else {
+                System.err.println("There is a problem with a configured Item!");
+            }
+        }
+        return inv;
+    }
+
+    public void prettyFill() {
+        for (int i = 0; i < 9; i++) {
+            prettyFill(i);
+        }
+        for (int i = rows * 9 - 9; i < rows * 9; i++) {
+            prettyFill(i);
+        }
+    }
+
+    private void prettyFill(int slot) {
+        if (items.containsKey(slot))
+            return;
+        items.put(slot, OrcItem.fill);
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+}

+ 150 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcItem.java

@@ -0,0 +1,150 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+public class OrcItem {
+
+    public static OrcItem enabled, disabled, coming_soon, back, fill, error = new OrcItem(Material.BARRIER, null,
+            "§cERROR: Item is wrong configured!", "§cPath in properties: see Displayname");
+
+    static {
+        fill = new OrcItem(VersionUtil.getGlassPlane("§8"));
+    }
+
+    private ItemStack is;
+    private OrcClickListener listener;
+    private DependListener depend;
+    private Runnable callback;
+
+    public OrcItem(Material mat, String display, String... lore) {
+        setItemStack(mat, display, lore);
+    }
+
+    public OrcItem(ItemStack is) {
+        setItemStack(is);
+    }
+
+    public OrcItem(Material mat, String display, List<String> lore) {
+        setItemStack(mat, (byte) 0, display, lore);
+    }
+
+    public OrcItem(Material mat) {
+        this(new ItemStack(mat));
+    }
+
+    public OrcItem(Material material, byte data, String display, ArrayList<String> lore) {
+        setItemStack(material, data, display, lore);
+    }
+
+    public void setCallback(Runnable r) {
+        callback = r;
+    }
+
+    @SuppressWarnings("deprecation")
+    public OrcItem setItemStack(Material mat, byte data, String display, List<String> lore) {
+        is = new ItemStack(mat, 1, data);
+        ItemMeta meta = is.getItemMeta();
+        meta.setDisplayName(display);
+        meta.setLore(lore);
+        meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
+        is.setItemMeta(meta);
+        return this;
+    }
+
+    public ItemStack getItemStack(Player p) {
+        if (p != null && depend != null) {
+            ItemStack is = depend.getItemStack(p);
+            if (is != null)
+                return is;
+        }
+        return is;
+    }
+
+    public ItemStack getItemStack() {
+        return is;
+    }
+
+    public OrcItem setItemStack(ItemStack is) {
+        Objects.requireNonNull(is, "ItemStack cannot be null");
+        this.is = is;
+        ItemMeta meta = is.getItemMeta();
+        meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
+        is.setItemMeta(meta);
+        return this;
+    }
+
+    public OrcItem setOnClick(OrcClickListener listener) {
+        this.listener = listener;
+        return this;
+    }
+
+    public OrcItem onClick(Player p, OrcInventory inv) {
+        if (listener != null) {
+            listener.onClick(p, inv, this);
+        }
+        if (callback != null)
+            callback.run();
+        return this;
+    }
+
+    public OrcItem setDisplay(String display) {
+        ItemMeta meta = is.getItemMeta();
+        meta.setDisplayName(display);
+        is.setItemMeta(meta);
+        return this;
+    }
+
+    public OrcItem setLore(String... lore) {
+        ItemMeta meta = is.getItemMeta();
+        meta.setLore(Arrays.asList(lore));
+        is.setItemMeta(meta);
+        return this;
+    }
+
+    public OrcItem removeLore() {
+        ItemMeta meta = is.getItemMeta();
+        meta.setLore(null);
+        is.setItemMeta(meta);
+        return this;
+    }
+
+    public OrcItem setItemStack(Material mat, String display, String... lore) {
+        return setItemStack(mat, (byte) 0, display, Arrays.asList(lore));
+    }
+
+    public OrcItem setDepend(DependListener listener) {
+        depend = listener;
+        return this;
+    }
+
+    public OrcItem clone() {
+        return new OrcItem(is);
+    }
+}

+ 71 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/OrcListener.java

@@ -0,0 +1,71 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+import de.butzlabben.missilewars.MissileWars;
+import java.util.HashMap;
+import java.util.UUID;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+
+/**
+ * @author Butzlabben
+ * @since 10.06.2018
+ */
+public class OrcListener implements Listener {
+
+    private static OrcListener instance;
+
+    private final HashMap<UUID, OrcInventory> invs = new HashMap<>();
+
+    private OrcListener() {
+        Bukkit.getPluginManager().registerEvents(this, MissileWars.getInstance());
+    }
+
+    public static synchronized OrcListener getInstance() {
+        if (instance == null)
+            instance = new OrcListener();
+        return instance;
+    }
+
+    @EventHandler
+    public void on(InventoryClickEvent e) {
+        if (e.getClickedInventory() != null && invs.containsKey(e.getWhoClicked().getUniqueId())) {
+            e.setCancelled(true);
+            OrcItem item = invs.get(e.getWhoClicked().getUniqueId()).items.get(e.getSlot());
+            if (item != null)
+                item.onClick((Player) e.getWhoClicked(), invs.get(e.getWhoClicked().getUniqueId()));
+        }
+    }
+
+    public void register(UUID uuid, OrcInventory inv) {
+        invs.put(uuid, inv);
+    }
+
+    @EventHandler
+    public void on(InventoryCloseEvent e) {
+        if (e.getInventory() != null) {
+            invs.remove(e.getPlayer().getUniqueId());
+        }
+    }
+}

+ 77 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/VoteInventory.java

@@ -0,0 +1,77 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.bukkit.Material;
+
+public class VoteInventory extends OrcInventory {
+
+    public VoteInventory(List<Arena> arenas) {
+        super(MessageConfig.getNativeMessage("vote.gui"), (int) Math.ceil(arenas.size() / 9D));
+
+        Map<Integer, int[]> map = new HashMap<>();
+        map.put(1, new int[] {4});
+        map.put(2, new int[] {0, 8});
+        map.put(3, new int[] {0, 4, 8});
+        map.put(4, new int[] {0, 3, 5, 8});
+        map.put(5, new int[] {0, 2, 4, 6, 8});
+        map.put(6, new int[] {0, 1, 3, 5, 7, 8});
+        map.put(7, new int[] {0, 1, 3, 4, 5, 7, 8});
+        map.put(8, new int[] {0, 1, 2, 3, 5, 6, 7, 8});
+        map.put(0, new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8});
+
+        final int rowCount = (int) Math.ceil(arenas.size() / 9D);
+        int currentRow = 0;
+        int inRowIndex = 0;
+        for (Arena arena : arenas) {
+            Material material;
+            try {
+                material = Material.valueOf(arena.getDisplayMaterial());
+            } catch (IllegalArgumentException ignored) {
+                Logger.WARN.log("Could not find a material with the name: " + arena.getDisplayMaterial());
+                material = Material.BARRIER;
+            }
+            OrcItem orcItem = new OrcItem(material, arena.getDisplayName());
+            orcItem.setOnClick((p, inv, item) -> {
+                p.performCommand("mw vote " + arena.getName());
+                p.closeInventory();
+            });
+            int index;
+            if (currentRow >= rowCount - 1) {
+                index = map.get(arenas.size() % 9)[inRowIndex];
+            } else {
+                index = map.get(0)[inRowIndex];
+            }
+            index += currentRow * 9;
+            addItem(index, orcItem);
+            inRowIndex++;
+            if (inRowIndex == 9) {
+                inRowIndex = 0;
+                currentRow++;
+            }
+        }
+    }
+}
+

+ 71 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/pages/InventoryPage.java

@@ -0,0 +1,71 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory.pages;
+
+import de.butzlabben.missilewars.inventory.OrcInventory;
+import de.butzlabben.missilewars.inventory.OrcItem;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+
+/**
+ * @author Butzlabben
+ * @since 20.05.2018
+ */
+public class InventoryPage extends OrcInventory {
+
+    InventoryPage next, before = null;
+    private int i = 0;
+
+    public InventoryPage(String title, int page, int pages) {
+        super(title, 6);
+
+        OrcItem oi = new OrcItem(VersionUtil.getSunFlower(), "§aPage §e" + page + " §aof§e " + pages);
+        addItem(5, 4, oi);
+
+        oi = new OrcItem(Material.PAPER, "§ePrevious page");
+        oi.setOnClick((p, inv, item) -> {
+            p.closeInventory();
+            p.openInventory(this.before.getInventory(p));
+        });
+        addItem(5, 0, oi);
+
+        oi = new OrcItem(Material.PAPER, "§eNext page");
+        oi.setOnClick((p, inv, item) -> {
+            p.closeInventory();
+            p.openInventory(this.next.getInventory(p));
+        });
+        addItem(5, 8, oi);
+    }
+
+    @Override
+    public Inventory getInventory(Player p) {
+        return super.getInventory(p);
+    }
+
+    public void addItem(OrcItem item) {
+        if (i > 36) {
+            System.err.println("More items than allowed in page view");
+            return;
+        }
+        addItem(i, item);
+        i++;
+    }
+}

+ 32 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/pages/ItemConverter.java

@@ -0,0 +1,32 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory.pages;
+
+
+import de.butzlabben.missilewars.inventory.OrcItem;
+
+/**
+ * @author Butzlabben
+ * @since 21.05.2018
+ */
+public interface ItemConverter<T> {
+
+    OrcItem convert(T element);
+
+}

+ 101 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/pages/PageGUICreator.java

@@ -0,0 +1,101 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.inventory.pages;
+
+
+import de.butzlabben.missilewars.inventory.OrcItem;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import org.bukkit.entity.Player;
+
+/**
+ * @author Butzlabben
+ * @since 21.05.2018
+ */
+public class PageGUICreator<T> {
+
+    private final int elementsPerPage;
+    private final String title;
+    @Getter
+    private final List<T> elements;
+    private final ItemConverter<T> converter;
+    private final Map<Integer, OrcItem> specialItems;
+    @Getter
+    private List<InventoryPage> invPages;
+
+    public PageGUICreator(String title, Collection<T> elements, ItemConverter<T> converter) {
+        this(title, elements, converter, Collections.emptyMap(), 4 * 9);
+    }
+
+    public PageGUICreator(String title, Collection<T> elements, ItemConverter<T> converter, Map<Integer, OrcItem> specialItems) {
+        this(title, elements, converter, specialItems, 4 * 9);
+    }
+
+    public PageGUICreator(String title, Collection<T> elements, ItemConverter<T> converter, Map<Integer, OrcItem> specialItems, int elementsPerPage) {
+        this.title = title;
+        this.elements = new ArrayList<>(elements);
+        this.converter = converter;
+        this.elementsPerPage = elementsPerPage;
+        this.specialItems = specialItems;
+    }
+
+    public void show(Player p) {
+        List<OrcItem> items = elements.stream().map(converter::convert).collect(Collectors.toList());
+        if (items.size() == 0)
+            return;
+
+        int pages = (int) (Math.ceil((items.size() / (double) elementsPerPage) < 1 ? 1 : Math.ceil((double) items.size() / (double) elementsPerPage)));
+
+        invPages = new ArrayList<>(pages);
+
+        for (int i = 1; i < pages + 1; i++) {
+            int start = i == 1 ? 0 : elementsPerPage * (i - 1);
+            int end = Math.min(items.size(), elementsPerPage * i);
+            List<OrcItem> page = items.subList(start, end);
+            InventoryPage invPage = new InventoryPage(title, i, pages);
+
+            page.forEach(invPage::addItem);
+            specialItems.forEach(invPage::addItem);
+
+            invPages.add(invPage);
+        }
+
+        for (int i = 0; i < invPages.size(); i++) {
+
+            int beforeIndex = i == 0 ? invPages.size() - 1 : i - 1;
+            int nextIndex = i == invPages.size() - 1 ? 0 : i + 1;
+
+            invPages.get(i).before = invPages.get(beforeIndex);
+            invPages.get(i).next = invPages.get(nextIndex);
+        }
+
+        if (p != null && p.isOnline())
+            p.openInventory(invPages.get(0).getInventory(p));
+    }
+
+    public void reopen(Player player) {
+        player.closeInventory();
+        show(player);
+    }
+}

+ 94 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/EndListener.java

@@ -0,0 +1,94 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.PlayerDataProvider;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaJoinEvent;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.entity.PlayerDeathEvent;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.scoreboard.Scoreboard;
+
+/**
+ * @author Butzlabben
+ * @since 15.01.2018
+ */
+public class EndListener extends GameBoundListener {
+
+    public EndListener(Game game) {
+        super(game);
+    }
+
+    @SuppressWarnings("deprecation")
+    @EventHandler
+    public void onJoin(PlayerArenaJoinEvent e) {
+        Game game = e.getGame();
+        if (game != getGame())
+            return;
+        Player p = e.getPlayer();
+        PlayerDataProvider.getInstance().storeInventory(p);
+        p.sendMessage(MessageConfig.getMessage("spectator"));
+
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.teleport(game.getArena().getSpectatorSpawn()), 2);
+
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.setGameMode(GameMode.SPECTATOR), 35);
+        Scoreboard sb = game.getScoreboard();
+        p.setScoreboard(sb);
+        sb.getTeam("2Guest§7").addPlayer(p);
+        p.setDisplayName("§7" + p.getName() + "§r");
+        game.addPlayer(p);
+    }
+
+    @EventHandler(priority = EventPriority.LOW)
+    public void onRespawn(PlayerRespawnEvent e) {
+        if (isInLobbyArea(e.getRespawnLocation())) {
+            e.setRespawnLocation(getGame().getArena().getSpectatorSpawn());
+        }
+    }
+
+    @EventHandler
+
+    public void onDeath(PlayerDeathEvent e) {
+        if (!isInLobbyArea(e.getEntity().getLocation()))
+            return;
+
+        Player p = e.getEntity();
+        p.setHealth(p.getMaxHealth());
+        p.teleport(getGame().getArena().getSpectatorSpawn());
+        e.setDeathMessage(null);
+    }
+
+    @EventHandler
+    public void onClick(InventoryClickEvent e) {
+        if (!(e.getWhoClicked() instanceof Player))
+            return;
+        Player p = (Player) e.getWhoClicked();
+        if (isInGameWorld(p.getLocation()))
+            if (p.getGameMode() != GameMode.CREATIVE && !p.isOp())
+                e.setCancelled(true);
+    }
+}

+ 44 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/GameBoundListener.java

@@ -0,0 +1,44 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener;
+
+import de.butzlabben.missilewars.game.Game;
+import org.bukkit.Location;
+import org.bukkit.event.Listener;
+
+public abstract class GameBoundListener implements Listener {
+
+    private final Game game;
+
+    protected GameBoundListener(Game game) {
+        this.game = game;
+    }
+
+    public boolean isInLobbyArea(Location location) {
+        return game.isInLobbyArea(location);
+    }
+
+    public boolean isInGameWorld(Location location) {
+        return game.getGameWorld().isWorld(location.getWorld());
+    }
+
+    public Game getGame() {
+        return game;
+    }
+}

+ 335 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/GameListener.java

@@ -0,0 +1,335 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.PlayerDataProvider;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.FallProtectionConfiguration;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaJoinEvent;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaLeaveEvent;
+import de.butzlabben.missilewars.wrapper.game.RespawnGoldBlock;
+import de.butzlabben.missilewars.wrapper.game.Shield;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import de.butzlabben.missilewars.wrapper.geometry.Plane;
+import de.butzlabben.missilewars.wrapper.missile.Missile;
+import de.butzlabben.missilewars.wrapper.missile.MissileFacing;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import java.util.Objects;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Fireball;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Projectile;
+import org.bukkit.entity.Snowball;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.block.BlockPhysicsEvent;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.event.entity.EntityExplodeEvent;
+import org.bukkit.event.entity.PlayerDeathEvent;
+import org.bukkit.event.entity.ProjectileLaunchEvent;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.scheduler.BukkitTask;
+import org.bukkit.scoreboard.Scoreboard;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Butzlabben
+ * @since 12.01.2018
+ */
+@SuppressWarnings("deprecation")
+public class GameListener extends GameBoundListener {
+
+    public GameListener(Game game) {
+        super(game);
+    }
+
+    @EventHandler
+    public void onMove(PlayerMoveEvent e) {
+        if (!isInGameWorld(e.getTo()))
+            return;
+        Player p = e.getPlayer();
+        if ((e.getTo().getBlockY() >= getGame().getArena().getMaxHeight()) && (p.getGameMode() == GameMode.SURVIVAL)) {
+            p.teleport(e.getFrom());
+            p.sendMessage(MessageConfig.getMessage("not_higher"));
+        } else if ((e.getTo().getBlockY() <= getGame().getArena().getDeathHeight()) && (p.getGameMode() == GameMode.SURVIVAL)) {
+            p.setLastDamageCause(new EntityDamageEvent(p, EntityDamageEvent.DamageCause.FALL, 20));
+            p.damage(20.0D);
+        }
+        if (!getGame().isInGameArea(e.getTo())) {
+            e.setCancelled(true);
+            Vector addTo = e.getFrom().toVector().subtract(e.getTo().toVector()).multiply(3);
+            addTo.setY(0);
+            p.teleport(e.getFrom().add(addTo));
+            p.sendMessage(MessageConfig.getMessage("arena_leave"));
+        }
+    }
+
+    @EventHandler
+    public void onExplode(EntityExplodeEvent e) {
+        if (!isInGameWorld(e.getLocation()))
+            return;
+        Game game = getGame();
+        if (e.getEntity().getType() == EntityType.FIREBALL && !game.getArena().getFireballConfiguration().isDestroysPortal())
+            e.blockList().removeIf(b -> b.getType() == VersionUtil.getPortal());
+    }
+
+    @EventHandler
+    public void on(BlockPhysicsEvent event) {
+        Location location = event.getBlock().getLocation();
+        if (!isInGameWorld(location)) return;
+        if (event.getChangedType() != VersionUtil.getPortal()) return;
+        Game game = getGame();
+        if (game.getArena().getPlane1().distance(location.toVector()) > game.getArena().getPlane2().distance(location.toVector())) {
+            game.getTeam1().win();
+            game.getTeam2().lose();
+        } else {
+            game.getTeam2().win();
+            game.getTeam1().lose();
+        }
+        game.setDraw(false);
+        game.stopGame();
+    }
+
+    @EventHandler
+    public void onInteract(PlayerInteractEvent e) {
+        if (!isInGameWorld(e.getPlayer().getLocation()))
+            return;
+        if (e.getItem() == null)
+            return;
+        if (e.getAction() != Action.RIGHT_CLICK_AIR && e.getAction() != Action.RIGHT_CLICK_BLOCK)
+            return;
+        Player player = e.getPlayer();
+        ItemStack itemStack = e.getItem();
+        Game game = getGame();
+
+        if (VersionUtil.isMonsterEgg(itemStack.getType())) {
+            e.setCancelled(true);
+            if (game.getArena().getMissileConfiguration().isOnlyBlockPlaceable() && e.getAction() != Action.RIGHT_CLICK_BLOCK) return;
+            if (game.getArena().getMissileConfiguration().isOnlyBlockPlaceable() &&
+                    !isInBetween(player.getLocation().toVector(), getGame().getArena().getPlane1(), getGame().getArena().getPlane2())) {
+                player.sendMessage(MessageConfig.getMessage("missile_place_deny"));
+                return;
+            }
+            Missile m = game.getArena().getMissileConfiguration().getMissileFromName(itemStack.getItemMeta().getDisplayName());
+            if (m == null) {
+                player.sendMessage(MessageConfig.getMessage("invalid_missile"));
+                return;
+            }
+            itemStack.setAmount(itemStack.getAmount() - 1);
+            player.setItemInHand(itemStack);
+            m.paste(player, MissileFacing.getFacingPlayer(player, game.getArena().getMissileConfiguration()), getGame());
+        } else if (itemStack.getType() == VersionUtil.getFireball()) {
+            int amount = e.getItem().getAmount();
+            e.getItem().setAmount(amount - 1);
+            if (amount == 1 && VersionUtil.getVersion() == 8) {
+                player.getInventory().remove(VersionUtil.getFireball());
+            }
+            Fireball fb = player.launchProjectile(Fireball.class);
+            fb.setVelocity(player.getLocation().getDirection().multiply(2.5D));
+            VersionUtil.playFireball(player, fb.getLocation());
+            fb.setYield(3F);
+            fb.setIsIncendiary(true);
+            fb.setBounce(false);
+        }
+    }
+
+    @EventHandler
+    public void onJoin(PlayerArenaJoinEvent e) {
+        Game game = e.getGame();
+        if (game != getGame())
+            return;
+
+        Player p = e.getPlayer();
+        MWPlayer mwPlayer = game.addPlayer(p);
+        PlayerDataProvider.getInstance().storeInventory(p);
+        p.getInventory().clear();
+
+        if (!game.getLobby().isJoinOngoingGame() || game.getPlayers().size() >= game.getLobby().getMaxSize()) {
+            p.sendMessage(MessageConfig.getMessage("spectator"));
+            Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.teleport(game.getArena().getSpectatorSpawn()), 2);
+            Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.setGameMode(GameMode.SPECTATOR), 35);
+            Scoreboard sb = game.getScoreboard();
+            p.setScoreboard(sb);
+            sb.getTeam("2Guest§7").addPlayer(p);
+            p.setDisplayName("§7" + p.getName() + "§r");
+        } else {
+            Team to;
+            int size1 = game.getTeam1().getMembers().size();
+            int size2 = game.getTeam2().getMembers().size();
+            if (size2 < size1)
+                to = getGame().getTeam2();
+            else
+                to = getGame().getTeam1();
+            to.addMember(mwPlayer);
+            p.sendMessage(MessageConfig.getMessage("team_assigned").replace("%team%", to.getFullname()));
+            to.updateIntervals(game.getArena().getInterval(to.getMembers().size()));
+            game.startForPlayer(p);
+            p.setScoreboard(game.getScoreboard());
+        }
+    }
+
+    @EventHandler
+    public void onThrow(ProjectileLaunchEvent e) {
+        if (!isInGameWorld(e.getEntity().getLocation()))
+            return;
+        Game game = getGame();
+        if (e.getEntity() instanceof Snowball) {
+            Snowball ball = (Snowball) e.getEntity();
+            if (ball.getShooter() instanceof Player) {
+                Shield shield = new Shield((Player) ball.getShooter(), game.getArena().getShieldConfiguration());
+                shield.onThrow(e);
+            }
+        }
+    }
+
+    @EventHandler
+    public void onDmg(EntityDamageByEntityEvent e) {
+        if (!isInGameWorld(e.getEntity().getLocation()))
+            return;
+        if (!(e.getEntity() instanceof Player))
+            return;
+        Player p = (Player) e.getEntity();
+        if (e.getDamager() instanceof Projectile) {
+            Projectile pj = (Projectile) e.getDamager();
+            Player shooter = (Player) pj.getShooter();
+            if (Objects.requireNonNull(getGame().getPlayer(shooter)).getTeam() == Objects.requireNonNull(getGame().getPlayer(p)).getTeam()) {
+                shooter.sendMessage(MessageConfig.getMessage("hurt_teammates"));
+                e.setCancelled(true);
+            }
+            return;
+        }
+        if (e.getDamager() instanceof Player) {
+            Player d = (Player) e.getDamager();
+            if (Objects.requireNonNull(getGame().getPlayer(d)).getTeam() == Objects.requireNonNull(getGame().getPlayer(p)).getTeam()) {
+                d.sendMessage(MessageConfig.getMessage("hurt_teammates"));
+                e.setCancelled(true);
+            }
+        }
+    }
+
+    @EventHandler(priority = EventPriority.HIGH)
+    public void onRespawn(PlayerRespawnEvent e) {
+        if (!isInGameWorld(e.getPlayer().getLocation()))
+            return;
+
+        Team t = Objects.requireNonNull(getGame().getPlayer(e.getPlayer())).getTeam();
+        if (t != null) {
+            e.setRespawnLocation(t.getSpawn());
+            FallProtectionConfiguration fallProtection = getGame().getArena().getFallProtection();
+            if (fallProtection.isEnabled())
+                new RespawnGoldBlock(e.getPlayer(), fallProtection.getDuration(), getGame());
+        } else {
+            e.setRespawnLocation(getGame().getArena().getSpectatorSpawn());
+        }
+    }
+
+    @EventHandler
+    public void onDeath(PlayerDeathEvent e) {
+        if (!isInGameWorld(e.getEntity().getLocation()))
+            return;
+        Game game = getGame();
+
+        Player p = e.getEntity();
+        e.setDeathMessage(MessageConfig.getNativeMessage("died").replace("%player%", p.getDisplayName()));
+        MWPlayer player = getGame().getPlayer(p);
+        assert player != null;
+
+        if (game.getArena().isAutoRespawn()) Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.spigot().respawn(), 20L);
+
+        if (player.getTeam() == null) {
+            p.setHealth(p.getMaxHealth());
+            p.teleport(getGame().getArena().getSpectatorSpawn());
+            e.setDeathMessage(null);
+            return;
+        }
+
+        p.setGameMode(GameMode.SURVIVAL);
+        if (p.getLastDamageCause() != null)
+            if (p.getLastDamageCause().getCause() == EntityDamageEvent.DamageCause.BLOCK_EXPLOSION
+                    || p.getLastDamageCause().getCause() == EntityDamageEvent.DamageCause.ENTITY_EXPLOSION) {
+                e.setDeathMessage(
+                        MessageConfig.getNativeMessage("died_explosion").replace("%player%", p.getDisplayName()));
+            }
+
+        String msg = e.getDeathMessage();
+        e.setDeathMessage(null);
+        getGame().broadcast(msg);
+    }
+
+    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+    public void onLeave(PlayerArenaLeaveEvent e) {
+        Game game = e.getGame();
+        if (game != getGame())
+            return;
+
+        MWPlayer player = getGame().getPlayer(e.getPlayer());
+        if (player == null) return;
+        BukkitTask task = game.getPlayerTasks().get(player.getUUID());
+        if (task != null) task.cancel();
+        Team team = player.getTeam();
+        if (team != null) {
+            getGame().broadcast(
+                    MessageConfig.getMessage("player_left").replace("%player%", e.getPlayer().getDisplayName()));
+            team.removeMember(getGame().getPlayer(e.getPlayer()));
+            int teamSize = team.getMembers().size();
+            if (teamSize == 0) {
+                Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> {
+                    getGame().draw(false);
+                    team.lose();
+                    team.getEnemyTeam().win();
+                    getGame().stopGame();
+                });
+                getGame().broadcast(MessageConfig.getMessage("team_offline").replace("%team%", team.getFullname()));
+            } else {
+                team.updateIntervals(game.getArena().getInterval(team.getMembers().size()));
+            }
+        }
+    }
+
+    @EventHandler
+    public void onClick(InventoryClickEvent event) {
+        if (!(event.getWhoClicked() instanceof Player)) return;
+        Player p = (Player) event.getWhoClicked();
+        if (!isInGameWorld(p.getLocation())) return;
+        if (p.getGameMode() == GameMode.CREATIVE || p.isOp()) return;
+        if (event.getSlotType() != InventoryType.SlotType.ARMOR) return;
+        event.setCancelled(true);
+    }
+
+
+    private boolean isInBetween(Vector point, Plane plane1, Plane plane2) {
+        double distanceBetween = plane1.distanceSquared(plane2.getSupport());
+        double distance1 = plane1.distanceSquared(point);
+        double distance2 = plane2.distanceSquared(point);
+        return distanceBetween > distance1 + distance2;
+    }
+}

+ 153 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/LobbyListener.java

@@ -0,0 +1,153 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.inventory.OrcItem;
+import de.butzlabben.missilewars.inventory.VoteInventory;
+import de.butzlabben.missilewars.util.PlayerDataProvider;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.abstracts.MapChooseProcedure;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaJoinEvent;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import de.butzlabben.missilewars.wrapper.signs.MWSign;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
+
+/**
+ * @author Butzlabben
+ * @since 11.01.2018
+ */
+public class LobbyListener extends GameBoundListener {
+
+    public LobbyListener(Game game) {
+        super(game);
+    }
+
+    @EventHandler
+    public void onClick(PlayerInteractEvent e) {
+        if (!isInLobbyArea(e.getPlayer().getLocation()))
+            return;
+        Player p = e.getPlayer();
+        if (p.getGameMode() == GameMode.CREATIVE)
+            return;
+        e.setCancelled(true);
+        if (e.getItem() == null) return;
+
+        if (VersionUtil.isStainedGlassPane(e.getItem().getType())) {
+            if (!p.hasPermission("mw.change")) return;
+            if (getGame().getTimer().getSeconds() < 10) {
+                p.sendMessage(MessageConfig.getMessage("change_team_not_now"));
+                return;
+            }
+            String displayName = e.getItem().getItemMeta().getDisplayName();
+            if (displayName.equals(getGame().getTeam1().getFullname())) {
+                p.performCommand("mw change 1");
+            } else {
+                p.performCommand("mw change 2");
+            }
+        } else if (e.getItem().getType() == Material.NETHER_STAR) {
+            VoteInventory inventory = new VoteInventory(getGame().getLobby().getArenas());
+            p.openInventory(inventory.getInventory(p));
+        }
+
+    }
+
+    @EventHandler
+    public void onJoin(PlayerArenaJoinEvent e) {
+        Game game = e.getGame();
+        if (game != getGame())
+            return;
+
+
+        Player p = e.getPlayer();
+        MWPlayer mw = game.addPlayer(p);
+
+        PlayerDataProvider.getInstance().storeInventory(p);
+
+        p.getInventory().clear();
+        p.setFoodLevel(20);
+        p.setHealth(p.getMaxHealth());
+        p.setScoreboard(game.getScoreboard());
+
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.setGameMode(GameMode.ADVENTURE), 10);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> p.teleport(game.getLobby().getSpawnPoint()), 2);
+
+        Team to;
+
+        int size1 = getGame().getTeam1().getMembers().size();
+        int size2 = getGame().getTeam2().getMembers().size();
+
+        if (size2 < size1)
+            to = getGame().getTeam2();
+        else
+            to = getGame().getTeam1();
+
+        // Premium version
+        if (p.hasPermission("mw.change")) {
+            p.getInventory().setItem(0, VersionUtil.getGlassPlane(getGame().getTeam1()));
+            p.getInventory().setItem(8, VersionUtil.getGlassPlane(getGame().getTeam2()));
+        }
+
+        if (game.getLobby().getMapChooseProcedure() == MapChooseProcedure.MAPVOTING && game.getArena() == null) {
+            p.getInventory().setItem(4, new OrcItem(Material.NETHER_STAR, "§3Vote Map").getItemStack());
+        }
+
+        to.addMember(mw);
+        p.sendMessage(MessageConfig.getMessage("team_assigned").replace("%team%", to.getFullname()));
+
+        String name = p.getName();
+        String players = "" + game.getPlayers().values().size();
+        String maxPlayers = "" + game.getLobby().getMaxSize();
+        String message = MessageConfig.getMessage("lobby_joined").replace("%max_players%", maxPlayers).replace("%players%", players).replace("%player%", name);
+        game.broadcast(message);
+
+        MissileWars.getInstance().getSignRepository().getSigns(game).forEach(MWSign::update);
+    }
+
+    @EventHandler
+    public void on(EntityDamageEvent e) {
+        if (isInLobbyArea(e.getEntity().getLocation())) e.setCancelled(true);
+    }
+
+    @EventHandler
+    public void on(PlayerRespawnEvent e) {
+        if (isInLobbyArea(e.getPlayer().getLocation())) e.setRespawnLocation(getGame().getLobby().getSpawnPoint());
+    }
+
+    @EventHandler
+    public void onClick(InventoryClickEvent e) {
+        if (!(e.getWhoClicked() instanceof Player))
+            return;
+        Player p = (Player) e.getWhoClicked();
+        if (isInLobbyArea(p.getLocation()))
+            if (p.getGameMode() != GameMode.CREATIVE && !p.isOp())
+                e.setCancelled(true);
+    }
+}

+ 226 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/PlayerListener.java

@@ -0,0 +1,226 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.game.GameState;
+import de.butzlabben.missilewars.util.MotdManager;
+import de.butzlabben.missilewars.util.PlayerDataProvider;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaJoinEvent;
+import de.butzlabben.missilewars.wrapper.event.PlayerArenaLeaveEvent;
+import de.butzlabben.missilewars.wrapper.event.PrePlayerArenaJoinEvent;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import de.butzlabben.missilewars.wrapper.signs.MWSign;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.event.entity.FoodLevelChangeEvent;
+import org.bukkit.event.player.PlayerDropItemEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import org.bukkit.event.player.PlayerPickupItemEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.event.server.ServerListPingEvent;
+import org.bukkit.scoreboard.Scoreboard;
+import org.bukkit.scoreboard.Team;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+@SuppressWarnings("deprecation")
+public class PlayerListener implements Listener {
+
+    @EventHandler
+    public void onPing(ServerListPingEvent event) {
+        if (Config.motdEnabled()) {
+            event.setMotd(MotdManager.getInstance().getMotd());
+        }
+    }
+
+    @EventHandler
+    public void on(FoodLevelChangeEvent e) {
+        Game game = getGame(e.getEntity().getLocation());
+        if (game != null)
+            e.setCancelled(true);
+    }
+
+    @EventHandler
+    public void on(BlockPlaceEvent e) {
+        Game game = getGame(e.getPlayer().getLocation());
+        if (game != null && e.getPlayer().getGameMode() != GameMode.CREATIVE)
+            e.setBuild(false);
+    }
+
+    @EventHandler
+    public void onDrop(PlayerDropItemEvent e) {
+        Game game = getGame(e.getPlayer().getLocation());
+        if (game != null)
+            e.setCancelled(true);
+    }
+
+    @EventHandler
+    public void onDrop(PlayerPickupItemEvent e) {
+        Game game = getGame(e.getPlayer().getLocation());
+        if (game != null)
+            e.setCancelled(true);
+    }
+
+    @EventHandler
+    public void on(PlayerQuitEvent e) {
+        Player p = e.getPlayer();
+
+        Game game = getGame(p.getLocation());
+        if (game != null) {
+            if (!game.isIn(game.getLobby().getAfterGameSpawn()))
+                p.teleport(game.getLobby().getAfterGameSpawn());
+            else
+                p.teleport(Config.getFallbackSpawn());
+        }
+    }
+
+    @EventHandler
+    public void on(PlayerJoinEvent event) {
+        Player p = event.getPlayer();
+
+        Game game = getGame(p.getLocation());
+        if (checkJoinOrLeave(p, null, game)) {
+            p.teleport(Config.getFallbackSpawn());
+        }
+    }
+
+    @EventHandler
+    public void onTeleport(PlayerTeleportEvent event) {
+        Game to = getGame(event.getTo());
+        Game from = getGame(event.getFrom());
+        if (checkJoinOrLeave(event.getPlayer(), from, to)) {
+            event.setCancelled(true);
+        }
+    }
+
+    @EventHandler
+    public void onMove(PlayerMoveEvent event) {
+        Game to = getGame(event.getTo());
+        Game from = getGame(event.getFrom());
+        if (checkJoinOrLeave(event.getPlayer(), from, to)) {
+            event.setCancelled(true);
+            Vector addTo = event.getFrom().toVector().subtract(event.getTo().toVector()).multiply(3);
+            addTo.setY(0);
+            event.getPlayer().teleport(event.getFrom().add(addTo));
+        }
+    }
+
+    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+    public void onLeaveWithStuff(PlayerArenaLeaveEvent event) {
+        Game game = event.getGame();
+
+        MWPlayer mwPlayer = game.getPlayer(event.getPlayer());
+        if (mwPlayer == null)
+            return;
+
+        if (mwPlayer.getTeam() != null)
+            mwPlayer.getTeam().removeMember(mwPlayer);
+
+        game.getPlayers().remove(event.getPlayer().getUniqueId());
+
+        Player player = mwPlayer.getPlayer();
+        if (player == null) {
+            Logger.WARN.log("Player was null as he left the game");
+            return;
+        }
+
+        Scoreboard sb = game.getScoreboard();
+        Team scoreboardTeam = sb.getPlayerTeam(player);
+        if (scoreboardTeam != null)
+            scoreboardTeam.removePlayer(player);
+
+        player.getInventory().clear();
+        player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
+
+        MissileWars.getInstance().getSignRepository().getSigns(game).forEach(MWSign::update);
+
+        PlayerDataProvider.getInstance().loadInventory(player);
+    }
+
+    @EventHandler
+    public void onPlayerArenaJoin(PlayerArenaJoinEvent event) {
+        Logger.DEBUG.log("PlayerArenaJoinEvent: " + event.getPlayer().getName());
+    }
+
+    @EventHandler
+    public void onPlayerArenaLeave(PlayerArenaLeaveEvent event) {
+        Logger.DEBUG.log("PlayerArenaLeaveEvent: " + event.getPlayer().getName());
+    }
+
+    private Game getGame(Location location) {
+        if (GameManager.getInstance() == null) return null;
+
+        return GameManager.getInstance().getGame(location);
+    }
+
+    /**
+     * Checks if cancelled and spits out events
+     *
+     * @param player
+     * @param from
+     * @param to
+     *
+     * @return
+     */
+    private boolean checkJoinOrLeave(Player player, Game from, Game to) {
+        if (to != null && to != from) {
+            PrePlayerArenaJoinEvent event = new PrePlayerArenaJoinEvent(player, to);
+            Bukkit.getPluginManager().callEvent(event);
+            if (event.isCancelled()) player.sendMessage(MessageConfig.getMessage("not_enter_arena"));
+            return event.isCancelled();
+        }
+        if (from != null && to != from) {
+            PlayerArenaLeaveEvent event = new PlayerArenaLeaveEvent(player, from);
+            Bukkit.getPluginManager().callEvent(event);
+        }
+        return false;
+    }
+
+
+    // Internal stuff
+    @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
+    public void onMaxPlayers(PrePlayerArenaJoinEvent event) {
+        if (event.getGame().getPlayers().size() >= event.getGame().getLobby().getMaxSize() && event.getGame().getState() == GameState.LOBBY)
+            event.setCancelled(true);
+    }
+
+    // Internal stuff
+    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+    public void on(PrePlayerArenaJoinEvent event) {
+        Logger.DEBUG.log("PrePlayerArenaJoinEvent: " + event.getPlayer().getName());
+        Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> Bukkit.getPluginManager().callEvent(new PlayerArenaJoinEvent(event.getPlayer(), event.getGame())));
+    }
+}

+ 54 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/signs/ClickListener.java

@@ -0,0 +1,54 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener.signs;
+
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.signs.MWSign;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.player.PlayerInteractEvent;
+
+@RequiredArgsConstructor
+public class ClickListener implements Listener {
+
+    @EventHandler
+    public void onJoinClick(PlayerInteractEvent event) {
+        if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
+            Block block = event.getClickedBlock();
+            if (VersionUtil.isWallSignMaterial(block.getType())) {
+                Location location = block.getLocation();
+                Optional<MWSign> optional = MissileWars.getInstance().getSignRepository().getSign(location);
+                if (!optional.isPresent())
+                    return;
+                MWSign sign = optional.get();
+                Game game = GameManager.getInstance().getGame(sign.getLobby());
+                if (game == null) return;
+                event.getPlayer().teleport(game.getLobby().getSpawnPoint());
+            }
+        }
+    }
+}

+ 85 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/signs/ManageListener.java

@@ -0,0 +1,85 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.listener.signs;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.signs.MWSign;
+import de.butzlabben.missilewars.wrapper.signs.SignRepository;
+import java.util.Optional;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.SignChangeEvent;
+
+public class ManageListener implements Listener {
+
+    @EventHandler
+    public void onSignChange(SignChangeEvent event) {
+        Player player = event.getPlayer();
+        if (!VersionUtil.isWallSignMaterial(event.getBlock().getType()))
+            return;
+        if (player.hasPermission("mw.sign.manage")) {
+            String headLine = event.getLine(0).toLowerCase();
+            if (headLine.equals("[missilewars]")) {
+                String lobbyName = event.getLine(1);
+                Game game = GameManager.getInstance().getGame(lobbyName);
+                if (game == null) {
+                    player.sendMessage(MessageConfig.getPrefix() + "§cCould not find lobby \"" + lobbyName + "\"");
+                    event.setCancelled(true);
+                } else {
+                    SignRepository signRepository = MissileWars.getInstance().getSignRepository();
+                    MWSign sign = new MWSign(event.getBlock().getLocation(), lobbyName);
+                    sign.update();
+                    signRepository.getSigns().add(new MWSign(event.getBlock().getLocation(), lobbyName));
+                    signRepository.save();
+                    player.sendMessage(MessageConfig.getPrefix() + "Sign was successfully created and connected");
+                }
+            }
+        }
+    }
+
+    @EventHandler
+    public void onBreak(BlockBreakEvent event) {
+        Player player = event.getPlayer();
+        if (player.hasPermission("mw.sign.manage")) {
+            Block block = event.getBlock();
+            if (VersionUtil.isWallSignMaterial(block.getType())) {
+                SignRepository repository = MissileWars.getInstance().getSignRepository();
+                Optional<MWSign> optional = repository.getSign(block.getLocation());
+                if (optional.isPresent()) {
+                    if (player.isSneaking()) {
+                        MWSign sign = optional.get();
+                        repository.getSigns().remove(sign);
+                        repository.save();
+                        player.sendMessage(MessageConfig.getPrefix() + "You have successfully removed this missilewars sign");
+                    } else {
+                        player.sendMessage(MessageConfig.getPrefix() + "§cYou have to be sneaking in order to remove this sign");
+                        event.setCancelled(true);
+                    }
+                }
+            }
+        }
+    }
+}

+ 120 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/ConnectionHolder.java

@@ -0,0 +1,120 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.bukkit.Bukkit;
+
+/**
+ * @author Butzlabben
+ * @since 13.08.2018
+ */
+public class ConnectionHolder {
+
+    private static final Object lock = new Object();
+    private static Connection connection;
+
+    private ConnectionHolder() {
+    }
+
+    public static void connect(String host, String database, String port, String user, String password) {
+        synchronized (lock) {
+            try {
+                Class.forName("com.mysql.jdbc.Driver");
+            } catch (ClassNotFoundException e) {
+                Logger.ERROR.log("[MySQL] §cDrivers are not working properly");
+                Bukkit.getPluginManager().disablePlugin(MissileWars.getInstance());
+                return;
+            }
+            try {
+                if (connection != null && !connection.isClosed())
+                    connection.close();
+                connection = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/" + database + "?user="
+                        + user + "&password=" + password);
+            } catch (SQLException e) {
+                Logger.ERROR.log("[MySQL] Failed to connect with given server:");
+                e.printStackTrace();
+                Bukkit.getPluginManager().disablePlugin(MissileWars.getInstance());
+            }
+        }
+    }
+
+    public static void close() {
+        synchronized (lock) {
+            try {
+                if (connection == null || connection.isClosed()) {
+                    System.err.println("[MySQL] Connection does not exist or was already closed");
+                    return;
+                }
+                connection.close();
+            } catch (SQLException e) {
+                Logger.ERROR.log("[MySQL] Connection could not be closed");
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static PreparedStatement prepareStatement(String sql) throws SQLException {
+        synchronized (lock) {
+            if (!isConnectionValid())
+                connect();
+            return connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
+        }
+    }
+
+    public static ResultSet executeQuery(PreparedStatement ps) throws SQLException {
+        synchronized (lock) {
+            if (!isConnectionValid())
+                connect();
+            return ps.executeQuery();
+        }
+    }
+
+    public static int executeUpdate(PreparedStatement ps) throws SQLException {
+        synchronized (lock) {
+            if (!isConnectionValid())
+                connect();
+            return ps.executeUpdate();
+        }
+    }
+
+    public static Connection getConnection() throws SQLException {
+        synchronized (lock) {
+            if (!isConnectionValid())
+                connect();
+            return connection;
+        }
+    }
+
+    public static void connect() {
+        connect(Config.getHost(), Config.getDatabase(), Config.getPort(), Config.getUser(), Config.getPassword());
+    }
+
+    public static boolean isConnectionValid() throws SQLException {
+        return connection != null && !connection.isClosed() && connection.isValid(5);
+    }
+}

+ 48 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/MathUtil.java

@@ -0,0 +1,48 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import org.bukkit.util.Vector;
+
+public class MathUtil {
+
+    /**
+     * Checks if two doubles are close enough to be considered "equal".
+     * As we have a limited precision, the normal "==" operator would make some of our math not working.
+     * As long as the difference is smaller than 1.0E-8D, it will return true. This value was chosen, as
+     * {@link org.bukkit.util.Vector#equals(Object)} uses a more losen tolerance.
+     *
+     * @param value1
+     * @param value2
+     *
+     * @return
+     */
+    public static boolean closeEnoughEquals(final double value1, final double value2) {
+        return Math.abs(value1 - value2) < 1.0E-8D;
+    }
+
+
+    public static boolean areMultiples(final Vector vector1, final Vector vector2) {
+        double factor = 0;
+        if (vector1.getX() != 0 && vector2.getX() != 0) factor = vector1.getX() / vector2.getX();
+        if (vector1.getY() != 0 && vector2.getY() != 0 && factor != 0) factor = vector1.getY() / vector2.getY();
+        if (vector1.getZ() != 0 && vector2.getZ() != 0 && factor != 0) factor = vector1.getZ() / vector2.getZ();
+        return vector1.equals(vector2.clone().multiply(factor));
+    }
+}

+ 73 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/MoneyUtil.java

@@ -0,0 +1,73 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import java.util.UUID;
+import net.milkbowl.vault.economy.Economy;
+import net.milkbowl.vault.economy.EconomyResponse;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.plugin.RegisteredServiceProvider;
+
+/**
+ * @author Butzlabben
+ * @since 13.08.2018
+ */
+public class MoneyUtil {
+
+    private static Object economy = null;
+
+    static {
+        if (Bukkit.getPluginManager().getPlugin("Vault") != null) {
+            try {
+                RegisteredServiceProvider<Economy> service = Bukkit.getServicesManager().getRegistration(Economy.class);
+                if (service != null)
+                    economy = service.getProvider();
+            } catch (Exception ignore) {
+            }
+
+        }
+
+        if (economy == null)
+            Logger.WARN.log("Couldn't find a Vault Economy extension");
+    }
+
+    private MoneyUtil() {
+    }
+
+    public static void giveMoney(UUID uuid, int money) {
+        if (money < 0)
+            return;
+        if (uuid == null)
+            return;
+        if (economy == null)
+            return;
+        OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
+        EconomyResponse r = ((Economy) economy).depositPlayer(op, money);
+        if (!r.transactionSuccess()) {
+            Logger.WARN.log("Couldn't give " + money + " to " + op.getName());
+            Logger.WARN.log("Message: " + r.errorMessage);
+        } else {
+            if (Bukkit.getPlayer(uuid) != null)
+                Bukkit.getPlayer(uuid).sendMessage(MessageConfig.getMessage("money").replace("%money%", money + ""));
+        }
+    }
+}

+ 61 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/MotdManager.java

@@ -0,0 +1,61 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameState;
+import org.bukkit.ChatColor;
+
+public class MotdManager {
+
+    private static final MotdManager instance = new MotdManager();
+    private String motd = "&cError in getting Motd";
+
+    public static MotdManager getInstance() {
+        return instance;
+    }
+
+    public String getMotd() {
+        return ChatColor.translateAlternateColorCodes('&', motd);
+    }
+
+    public void updateMOTD(Game game) {
+        GameState state = game.getState();
+
+        if (Config.motdEnabled()) {
+            String motd = "&cError in getting Motd";
+            switch (state) {
+                case LOBBY:
+                    motd = Config.motdLobby();
+                    break;
+                case END:
+                    motd = Config.motdEnded();
+                    break;
+                case INGAME:
+                    motd = Config.motdGame();
+                    break;
+            }
+
+            String players = "" + game.getPlayers().values().size();
+            String maxPlayers = "" + game.getLobby().getMaxSize();
+            this.motd = ChatColor.translateAlternateColorCodes('&', motd).replace("%max_players%", maxPlayers).replace("%players%", players);
+        }
+    }
+}

+ 86 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/PlayerDataProvider.java

@@ -0,0 +1,86 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.wrapper.player.PlayerData;
+import java.io.File;
+import java.util.HashMap;
+import java.util.UUID;
+import org.bukkit.entity.Player;
+
+public class PlayerDataProvider {
+
+    private static final PlayerDataProvider ourInstance = new PlayerDataProvider();
+
+    private final String path = "data";
+    private final HashMap<UUID, PlayerData> data = new HashMap<>();
+
+    private PlayerDataProvider() {
+    }
+
+    public static PlayerDataProvider getInstance() {
+        return ourInstance;
+    }
+
+    public void storeInventory(Player player) {
+        if (hasData(player.getUniqueId()))
+            return;
+        PlayerData playerData = new PlayerData(player);
+        data.put(player.getUniqueId(), playerData);
+        playerData.saveToFile(getPathFromUUID(player.getUniqueId()).getPath());
+    }
+
+    public void loadInventory(Player player) {
+        PlayerData data;
+        File file = getPathFromUUID(player.getUniqueId());
+        if (this.data.containsKey(player.getUniqueId())) {
+            data = this.data.remove(player.getUniqueId());
+        } else {
+            if (file.exists()) {
+                data = PlayerData.loadFromFile(file);
+            } else {
+                player.getInventory().clear();
+                player.setLevel(0);
+                Logger.WARN.log("Could not find inventory for " + player.getUniqueId());
+                return;
+            }
+        }
+        if (file.exists())
+            file.delete();
+        if (data != null) {
+            data.apply(player);
+        }
+    }
+
+    public File getPathFromUUID(UUID uuid) {
+        File file = new File(MissileWars.getInstance().getDataFolder(), path);
+        file.mkdirs();
+        return new File(file, uuid.toString() + ".yml");
+    }
+
+    public boolean hasData(UUID uuid) {
+        if (data.containsKey(uuid))
+            return true;
+        File file = getPathFromUUID(uuid);
+        return file.exists() && file.isFile();
+
+    }
+}

+ 114 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/Randomizer.java

@@ -0,0 +1,114 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.MissileConfiguration;
+import de.butzlabben.missilewars.wrapper.missile.Missile;
+import de.butzlabben.missilewars.wrapper.player.Interval;
+import java.util.HashMap;
+import java.util.Random;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+/**
+ * @author Butzlabben
+ * @since 19.01.2018
+ */
+public class Randomizer {
+
+    private final MissileConfiguration missileConfiguration;
+    private final Game game;
+    private final HashMap<Interval, String> defensive = new HashMap<>();
+    private final HashMap<Interval, String> missiles = new HashMap<>();
+
+    private int allMissiles = 0;
+    private int allDefensives = 0;
+    private int count = 1;
+
+    public Randomizer(Game game) {
+        this.game = game;
+        missileConfiguration = game.getArena().getMissileConfiguration();
+        for (Missile missile : missileConfiguration.getMissiles()) {
+            Interval i = new Interval(allMissiles, allMissiles + missile.occurrence() - 1);
+            missiles.put(i, missile.getName());
+            allMissiles += missile.occurrence();
+        }
+
+        int shieldOccurrence = game.getArena().getShieldConfiguration().getOccurrence();
+        Interval shield = new Interval(allDefensives, allDefensives + shieldOccurrence - 1);
+        allDefensives += shieldOccurrence;
+        defensive.put(shield, "s");
+
+        int arrowOccurrence = game.getArena().getArrowOccurrence();
+        Interval arrow = new Interval(allDefensives, allDefensives + arrowOccurrence - 1);
+        allDefensives += arrowOccurrence;
+        defensive.put(arrow, "a");
+
+        Interval fireball = new Interval(allDefensives, allDefensives + arrowOccurrence - 1);
+        allDefensives += arrowOccurrence;
+        defensive.put(fireball, "f");
+    }
+
+    public ItemStack createItem() {
+        ItemStack is = null;
+        ItemMeta im;
+        Random r = new Random();
+        if (count == 2) {
+            count = 0;
+            int random = r.nextInt(allDefensives);
+            for (Interval i : defensive.keySet()) {
+                if (i.isIn(random)) {
+                    String to = defensive.get(i);
+                    if (to.equals("s")) {
+                        is = new ItemStack(VersionUtil.getSnowball());
+                        im = is.getItemMeta();
+                        im.setDisplayName(game.getArena().getShieldConfiguration().getName());
+                    } else if (to.equals("a")) {
+                        is = new ItemStack(Material.ARROW, 3);
+                        im = is.getItemMeta();
+                    } else {
+                        is = new ItemStack(VersionUtil.getFireball());
+                        im = is.getItemMeta();
+                        im.setDisplayName(game.getArena().getFireballConfiguration().getName());
+                    }
+                    is.setItemMeta(im);
+                    return is;
+                }
+            }
+        } else {
+            int random = r.nextInt(allMissiles);
+            for (Interval i : missiles.keySet()) {
+                if (i.isIn(random)) {
+                    String to = missiles.get(i);
+                    Missile m = missileConfiguration.getMissileFromName(to);
+                    if (m != null) {
+                        is = m.getItem();
+                    } else
+                        Logger.DEBUG.log("There wasn't a missile found, when giving out items");
+                    ++count;
+                }
+            }
+        }
+        return is;
+    }
+}

+ 76 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/ScoreboardManager.java

@@ -0,0 +1,76 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.game.Game;
+import java.util.HashMap;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.scoreboard.DisplaySlot;
+import org.bukkit.scoreboard.Objective;
+import org.bukkit.scoreboard.Scoreboard;
+
+@RequiredArgsConstructor
+public class ScoreboardManager {
+
+    private final Game game;
+    private final Scoreboard board;
+
+    public void updateInGameScoreboard() {
+        removeScoreboard();
+
+        Objective obj = board.registerNewObjective("Info", "dummy");
+        obj.setDisplaySlot(DisplaySlot.SIDEBAR);
+        obj.setDisplayName(Config.getScoreboardTitle());
+
+        HashMap<String, Integer> entries = Config.getScoreboardEntries();
+
+        for (String entry : entries.keySet()) {
+            String s = rep(entry);
+            obj.getScore(s).setScore(entries.get(entry));
+        }
+    }
+
+    public void removeScoreboard() {
+        Objective old = board.getObjective(DisplaySlot.SIDEBAR);
+        if (old != null)
+            old.unregister();
+    }
+
+    private String rep(String entry) {
+        return replaceTeam1(replaceTeam2(replaceTime(entry)));
+    }
+
+
+    private String replaceTeam2(String str) {
+        return str.replace("%team2%", game.getTeam2().getFullname())
+                .replace("%team2_amount%", "" + game.getTeam2().getMembers().size())
+                .replace("%team2_color%", game.getTeam2().getColorCode());
+    }
+
+    private String replaceTeam1(String str) {
+        return str.replace("%team1%", game.getTeam1().getFullname())
+                .replace("%team1_amount%", "" + game.getTeam1().getMembers().size())
+                .replace("%team1_color%", game.getTeam1().getColorCode());
+    }
+
+    private String replaceTime(String str) {
+        return str.replace("%time%", "" + game.getTimer().getSeconds() / 60);
+    }
+}

+ 217 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/SetupUtil.java

@@ -0,0 +1,217 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.wrapper.abstracts.Arena;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import org.bukkit.plugin.java.JavaPlugin;
+
+/**
+ * @author Butzlabben
+ * @since 14.08.2018
+ */
+public class SetupUtil {
+
+    private static final int BUFFER_SIZE = 4096;
+
+    private SetupUtil() {
+    }
+
+    public static void checkShields() {
+        for (Arena arena : Arenas.getArenas()) {
+            File file = new File(MissileWars.getInstance().getDataFolder(), arena.getShieldConfiguration().getSchematic());
+            if (!file.isFile()) {
+                Logger.BOOT.log("Copying default shield schematic");
+
+                String resource = "shield.schematic";
+                copyFile(resource, file.getPath());
+
+            }
+        }
+    }
+
+    public static void checkMap(String worldName) {
+        File file = new File(Config.getArenaFolder() + "/" + worldName);
+        if (!file.isDirectory()) {
+
+            Logger.WARN.log("There was no map found with the name \"" + worldName + "\"");
+            Logger.BOOT.log("Copying default map");
+
+            String resource = "MissileWars-Arena.zip";
+
+            try {
+                copyZip(resource, file.getPath());
+            } catch (IOException e) {
+                Logger.ERROR.log("Unable to copy new map");
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static void checkMissiles() {
+        File file = new File(MissileWars.getInstance().getDataFolder(), "missiles");
+        if (!file.isDirectory()) {
+            Logger.BOOT.log("Copying default missiles folder");
+
+            String resource = "missiles.zip";
+
+            try {
+                copyZip(resource, file.getPath());
+            } catch (IOException e) {
+                Logger.ERROR.log("Unable to copy missiles");
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private static void copyFile(String resource, String out) {
+        File file = new File(out);
+        if (!file.exists()) {
+            try {
+                InputStream in = MissileWars.getInstance().getResource(resource);
+                Files.copy(in, file.toPath());
+            } catch (IOException e) {
+                System.err.println("Wasn't able to create Config");
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private static void copyFolder(String resource, String outputFolder) throws IOException {
+        copyResourcesToDirectory(jarForClass(MissileWars.class, null), resource, outputFolder);
+    }
+
+    public static void copyZip(String resource, String outputFolder) throws IOException {
+        File out = new File(MissileWars.getInstance().getDataFolder(), resource);
+
+        InputStream in = JavaPlugin.getPlugin(MissileWars.class).getResource(resource);
+
+        Files.copy(in, out.toPath());
+
+        unzip(out.getPath(), outputFolder);
+        out.deleteOnExit();
+    }
+
+    public static void unzip(String zipFilePath, String destDirectory) throws IOException {
+        File destDir = new File(destDirectory);
+        if (!destDir.exists()) {
+            destDir.mkdir();
+        }
+        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
+
+        ZipEntry entry = zipIn.getNextEntry();
+        // iterates over entries in the zip file
+        while (entry != null) {
+            String filePath = destDirectory + File.separator + entry.getName();
+            if (!entry.isDirectory()) {
+                // if the entry is a file, extracts it
+                extractFile(zipIn, filePath);
+            } else {
+                // if the entry is a directory, make the directory
+                File dir = new File(filePath);
+                dir.mkdir();
+            }
+            zipIn.closeEntry();
+            entry = zipIn.getNextEntry();
+        }
+        zipIn.close();
+    }
+
+    /**
+     * Extracts a zip entry (file entry)
+     *
+     * @param zipIn
+     * @param filePath
+     *
+     * @throws IOException
+     */
+    private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
+        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
+        byte[] bytesIn = new byte[BUFFER_SIZE];
+        int read;
+        while ((read = zipIn.read(bytesIn)) != -1) {
+            bos.write(bytesIn, 0, read);
+        }
+        bos.close();
+    }
+
+    public static JarFile jarForClass(Class<?> clazz, JarFile defaultJar) {
+        String path = "/" + clazz.getName().replace('.', '/') + ".class";
+        URL jarUrl = clazz.getResource(path);
+        if (jarUrl == null) {
+            return defaultJar;
+        }
+
+        String url = jarUrl.toString();
+        int bang = url.indexOf("!");
+        String JAR_URI_PREFIX = "jar:file:";
+        if (url.startsWith(JAR_URI_PREFIX) && bang != -1) {
+            try {
+                return new JarFile(url.substring(JAR_URI_PREFIX.length(), bang));
+            } catch (IOException e) {
+                throw new IllegalStateException("Error loading jar file.", e);
+            }
+        } else {
+            return defaultJar;
+        }
+    }
+
+    /**
+     * Copies a directory from a jar file to an external directory.
+     */
+    public static void copyResourcesToDirectory(JarFile fromJar, String jarDir, String destDir) throws IOException {
+        for (Enumeration<JarEntry> entries = fromJar.entries(); entries.hasMoreElements(); ) {
+            JarEntry entry = entries.nextElement();
+            if (entry.getName().startsWith(jarDir + "/") && !entry.isDirectory()) {
+                File dest = new File(destDir + "/" + entry.getName().substring(jarDir.length() + 1));
+                File parent = dest.getParentFile();
+                if (parent != null) {
+                    parent.mkdirs();
+                }
+
+                try (FileOutputStream out = new FileOutputStream(dest); InputStream in = fromJar.getInputStream(entry)) {
+                    byte[] buffer = new byte[8 * 1024];
+
+                    int s;
+                    while ((s = in.read(buffer)) > 0) {
+                        out.write(buffer, 0, s);
+                    }
+                } catch (IOException e) {
+                    throw new IOException("Could not copy asset from jar file", e);
+                }
+            }
+        }
+    }
+}

+ 93 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/serialization/LocationTypeAdapter.java

@@ -0,0 +1,93 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.serialization;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import de.butzlabben.missilewars.Logger;
+import java.io.IOException;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+
+public class LocationTypeAdapter extends TypeAdapter<Location> {
+
+    private final boolean serializeWorld;
+
+    public LocationTypeAdapter(boolean serializeWorld) {
+        this.serializeWorld = serializeWorld;
+    }
+
+    public LocationTypeAdapter() {
+        this(false);
+    }
+
+    @Override
+    public void write(JsonWriter out, Location value) throws IOException {
+        out.beginObject();
+        if (serializeWorld && value.getWorld() != null) {
+            out.name("world").value(value.getWorld().getName());
+        }
+        out.name("x").value(value.getX());
+        out.name("y").value(value.getY());
+        out.name("z").value(value.getZ());
+        out.name("yaw").value(value.getYaw());
+        out.name("pitch").value(value.getPitch());
+        out.endObject();
+    }
+
+    @Override
+    public Location read(JsonReader in) throws IOException {
+        Location location = new Location(null, 0, 0, 0);
+
+        in.beginObject();
+        while (in.hasNext()) {
+            switch (in.nextName()) {
+                case "world":
+                    if (!serializeWorld) break;
+                    String worldName = in.nextString();
+                    World world = Bukkit.getWorld(worldName);
+                    if (world == null) {
+                        Logger.WARN.log("Could not find world \"" + worldName + "\" which is specified at one missilewars sign");
+                    }
+                    location.setWorld(world);
+                    break;
+                case "x":
+                    location.setX(in.nextDouble());
+                    break;
+                case "y":
+                    location.setY(in.nextDouble());
+                    break;
+                case "z":
+                    location.setZ(in.nextDouble());
+                    break;
+                case "yaw":
+                    location.setYaw((float) in.nextDouble());
+                    break;
+                case "pitch":
+                    location.setPitch((float) in.nextDouble());
+                    break;
+            }
+        }
+
+        in.endObject();
+        return location;
+    }
+}

+ 147 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/serialization/Serializer.java

@@ -0,0 +1,147 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.serialization;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.google.common.base.Preconditions;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import de.butzlabben.missilewars.Logger;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.apache.commons.lang.Validate;
+import org.bukkit.Location;
+import org.bukkit.World;
+
+public class Serializer {
+
+    private static final Gson gson;
+
+    static {
+        gson = new GsonBuilder()
+                .registerTypeAdapter(Location.class, new LocationTypeAdapter())
+//                .registerTypeAdapter(EntityType.class, new EntityTypeTypeAdapter())
+                .setPrettyPrinting()
+                .create();
+    }
+
+    public static void serialize(File file, Object object) throws IOException {
+        try {
+            correctEnums(object);
+        } catch (Exception e) {
+            Logger.WARN.log("Could not correct null enum values");
+            e.printStackTrace();
+        }
+        String json = gson.toJson(object);
+        String yaml = jsonToYaml(json);
+        yaml = replaceColorStrings('§', '&', yaml);
+        String oldYaml = new String(Files.readAllBytes(Paths.get(file.getAbsolutePath())));
+        if (!oldYaml.equals(yaml))
+            Files.write(Paths.get(file.getAbsolutePath()), yaml.getBytes());
+    }
+
+    public static <T> T deserialize(File file, Class<T> clazz) throws IOException {
+        String yaml = new String(Files.readAllBytes(Paths.get(file.getAbsolutePath())));
+        yaml = replaceColorStrings('&', '§', yaml);
+        String json = yamlToJson(yaml);
+        json = json.replace("max_X", "max_x");
+        return gson.fromJson(json, clazz);
+    }
+
+    public static void setWorldAtAllLocations(Object object, World world) throws Exception {
+        setWorldAtAllLocations(object, world, 0);
+    }
+
+    public static void setWorldAtAllLocations(Object object, World world, int depthCount) throws Exception {
+        if (object == null) return;
+        Preconditions.checkNotNull(world);
+
+        Class<?> clazz = object.getClass();
+        // Return here, as we only need to deserialize our own projects
+        if (!clazz.getName().contains("de.butzlabben")) return;
+
+        for (Field field : clazz.getDeclaredFields()) {
+            field.setAccessible(true);
+            if (field.getType() != Location.class) {
+                if (depthCount < 5)
+                    setWorldAtAllLocations(field.get(object), world, depthCount + 1);
+                continue;
+            }
+            Location location = (Location) field.get(object);
+            if (location == null) continue;
+            if (location.getWorld() != null) continue;
+
+            location.setWorld(world);
+        }
+    }
+
+    // Sets all null enums to their first value
+    private static void correctEnums(Object object) throws Exception {
+        Preconditions.checkNotNull(object);
+
+        Class<?> clazz = object.getClass();
+        for (Field field : clazz.getDeclaredFields()) {
+            field.setAccessible(true);
+            if (!field.getType().isEnum()) continue;
+            if (field.get(object) != null) continue;
+            field.set(object, getEnumValues(field.getType())[0]);
+        }
+    }
+
+    private static String jsonToYaml(String json) throws IOException {
+        JsonNode jsonNodeTree = new ObjectMapper().readTree(json);
+        return new YAMLMapper().writeValueAsString(jsonNodeTree);
+    }
+
+    private static String yamlToJson(String yaml) throws IOException {
+        ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
+        Object obj = yamlReader.readValue(yaml, Object.class);
+        ObjectMapper jsonWriter = new ObjectMapper();
+        return jsonWriter.writeValueAsString(obj);
+    }
+
+    private static Object[] getEnumValues(Class<?> enumClass)
+            throws NoSuchFieldException, IllegalAccessException {
+        Field f = enumClass.getDeclaredField("$VALUES");
+        f.setAccessible(true);
+        Object o = f.get(null);
+        return (Object[]) o;
+    }
+
+    private static String replaceColorStrings(char replace, char replacement, String textToTranslate) {
+        Validate.notNull(textToTranslate, "Cannot translate null text");
+        char[] b = textToTranslate.toCharArray();
+
+        for (int i = 0; i < b.length - 1; ++i) {
+            if (b[i] == replace && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[i + 1]) > -1) {
+                b[i] = replacement;
+                b[i + 1] = Character.toLowerCase(b[i + 1]);
+            }
+        }
+
+        return new String(b);
+
+    }
+}

+ 177 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/GameProfileBuilder.java

@@ -0,0 +1,177 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.stats;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import com.mojang.authlib.properties.PropertyMap;
+import com.mojang.util.UUIDTypeAdapter;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
+
+/**
+ * @author Butzlabben
+ * @since 26.02.2018
+ */
+public class GameProfileBuilder {
+
+    private static final Gson gson = new GsonBuilder().disableHtmlEscaping()
+            .registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
+            .registerTypeAdapter(GameProfile.class, new GameProfileSerializer())
+            .registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()).create();
+    @Getter
+    private static final HashMap<UUID, CachedProfile> cache = new HashMap<>();
+    private static final Object sync = new Object();
+    private static long cacheTime = -1L;
+
+    public static GameProfile fetch(UUID uuid) throws IOException {
+        return fetch(uuid, false);
+    }
+
+    public static GameProfile fetch(UUID uuid, boolean forceNew) throws IOException {
+        if (!forceNew && cache.containsKey(uuid) && cache.get(uuid).isValid()) {
+            return cache.get(uuid).profile;
+        }
+
+        String json = getText(String.format("https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false",
+                UUIDTypeAdapter.fromUUID(uuid)));
+
+        try {
+            GameProfile result = gson.fromJson(json, GameProfile.class);
+            cache.put(uuid, new CachedProfile(result));
+            return result;
+        } catch (Exception exception) {
+            throw new IOException("Could not read response: " + json);
+        }
+    }
+
+    public static GameProfile getProfile(UUID uuid, String name, String skin) {
+        return getProfile(uuid, name, skin, null);
+    }
+
+    public static GameProfile getProfile(UUID uuid, String name, String skinUrl, String capeUrl) {
+        GameProfile profile = new GameProfile(uuid, name);
+        boolean cape = (capeUrl != null) && (!capeUrl.isEmpty());
+
+        List<Object> args = new ArrayList<>();
+        args.add(System.currentTimeMillis());
+        args.add(UUIDTypeAdapter.fromUUID(uuid));
+        args.add(name);
+        args.add(skinUrl);
+        if (cape) {
+            args.add(capeUrl);
+        }
+        profile.getProperties().put("textures",
+                new Property("textures",
+                        Base64Coder.encodeString(String.format(
+                                cape ? "{\"timestamp\":%d,\"profileId\":\"%s\",\"profileName\":\"%s\",\"isPublic\":true,\"textures\":{\"SKIN\":{\"url\":\"%s\"},\"CAPE\":{\"url\":\"%s\"}}}"
+                                        : "{\"timestamp\":%d,\"profileId\":\"%s\",\"profileName\":\"%s\",\"isPublic\":true,\"textures\":{\"SKIN\":{\"url\":\"%s\"}}}",
+                                args.toArray(new Object[0])))));
+        return profile;
+    }
+
+    public static void setCacheTime(long time) {
+        cacheTime = time;
+    }
+
+    public static String getText(String url) throws IOException {
+        URL website = new URL(url);
+        URLConnection connection = website.openConnection();
+        BufferedReader in = new BufferedReader(
+                new InputStreamReader(
+                        connection.getInputStream()));
+
+        StringBuilder response = new StringBuilder();
+        String inputLine;
+
+        while ((inputLine = in.readLine()) != null) {
+            response.append(inputLine);
+        }
+
+        in.close();
+
+        return response.toString();
+    }
+
+    private static class GameProfileSerializer implements JsonSerializer<GameProfile>, JsonDeserializer<GameProfile> {
+
+        public GameProfile deserialize(JsonElement json, Type type, JsonDeserializationContext context)
+                throws JsonParseException {
+            JsonObject object = (JsonObject) json;
+            UUID id = object.has("id") ? (UUID) context.deserialize(object.get("id"), UUID.class) : null;
+            String name = object.has("name") ? object.getAsJsonPrimitive("name").getAsString() : null;
+            GameProfile profile = new GameProfile(id, name);
+            if (object.has("properties")) {
+                for (Map.Entry<String, Property> prop : ((PropertyMap) context.deserialize(object.get("properties"),
+                        PropertyMap.class)).entries()) {
+                    profile.getProperties().put(prop.getKey(), prop.getValue());
+                }
+            }
+            return profile;
+        }
+
+        public JsonElement serialize(GameProfile profile, Type type, JsonSerializationContext context) {
+            JsonObject result = new JsonObject();
+            if (profile.getId() != null) {
+                result.add("id", context.serialize(profile.getId()));
+            }
+            if (profile.getName() != null) {
+                result.addProperty("name", profile.getName());
+            }
+            if (!profile.getProperties().isEmpty()) {
+                result.add("properties", context.serialize(profile.getProperties()));
+            }
+            return result;
+        }
+    }
+
+    public static class CachedProfile {
+
+        @Getter
+        private final GameProfile profile;
+
+        public CachedProfile(GameProfile profile) {
+            this.profile = profile;
+        }
+
+        public boolean isValid() {
+            return GameProfileBuilder.cacheTime < 0L;
+        }
+    }
+}

+ 141 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/PlayerGuiFactory.java

@@ -0,0 +1,141 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.stats;
+
+import com.mojang.authlib.GameProfile;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.inventory.OrcItem;
+import de.butzlabben.missilewars.inventory.pages.PageGUICreator;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.stats.PlayerStats;
+import de.butzlabben.missilewars.wrapper.stats.PlayerStatsComparator;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ForkJoinPool;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+
+@Getter
+public class PlayerGuiFactory {
+
+    private final static int MAX_FETCHES = 9;
+    private final static int FETCH_EVERY_ROUND = 3;
+
+    private final List<PlayerStats> stats;
+    private final Map<UUID, String> names = new ConcurrentHashMap<>();
+    private final int fetchRound = 0;
+
+    public PlayerGuiFactory(List<PlayerStats> stats) {
+        this.stats = stats;
+        for (PlayerStats stat : stats) {
+            OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(stat.getUuid());
+            if (offlinePlayer.getName() != null) {
+                names.put(stat.getUuid(), offlinePlayer.getName());
+                stat.setName(offlinePlayer.getName());
+            }
+            if (GameProfileBuilder.getCache().containsKey(stat.getUuid())) {
+                String name = GameProfileBuilder.getCache().get(stat.getUuid()).getProfile().getName();
+                names.put(stat.getUuid(), name);
+                stat.setName(name);
+            }
+        }
+    }
+
+    public void openWhenReady(Player player) {
+        int realSize = stats.size();
+        int currentSize = names.size();
+        if (realSize > currentSize) {
+            if (Config.isContactAuth()) {
+                player.sendMessage(MessageConfig.getPrefix() + "Fetching not cached player names: " + currentSize + "/" + realSize);
+                ForkJoinPool.commonPool().execute(() -> {
+                    List<UUID> missing = getMissingUUIDs();
+                    int maxFetches = Math.min(missing.size(), MAX_FETCHES);
+                    for (int i = 0; i < maxFetches; i++) {
+                        UUID uuid = missing.get(i);
+                        try {
+                            GameProfile profile = GameProfileBuilder.fetch(uuid);
+                            names.put(uuid, profile.getName());
+                        } catch (IOException e) {
+                            names.put(uuid, "Error getting name");
+                            if (!e.getMessage().contains("Could not connect to mojang servers for unknown player"))
+                                Logger.WARN.log("Could not fetch name for " + uuid.toString() + ". Reason: " + e.getMessage());
+                        }
+                    }
+
+                    openWhenReady(player);
+                });
+                return;
+            }
+        }
+        for (PlayerStats stat : stats) {
+            stat.setName(names.get(stat.getUuid()));
+            if (stat.getName() == null || stat.getName().equals("")) {
+                Logger.WARN.log("Could not find name for: " + stat.getUuid());
+            }
+        }
+
+        Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> open(player));
+    }
+
+    public List<UUID> getMissingUUIDs() {
+        return stats.stream().map(PlayerStats::getUuid).filter(uuid -> !names.containsKey(uuid)).collect(Collectors.toList());
+    }
+
+    private void open(Player player) {
+        List<PlayerStats> stats = new ArrayList<>(this.stats);
+        stats.sort(new PlayerStatsComparator());
+
+        PageGUICreator<PlayerStats> creator = new PageGUICreator<>("§ePlayer statistics", stats, (item) -> {
+            String name = item.getName();
+            ItemStack itemStack = new ItemStack(VersionUtil.getPlayerSkullMaterial());
+            SkullMeta sm = (SkullMeta) itemStack.getItemMeta();
+            if (Config.isShowRealSkins()) {
+                //noinspection deprecation
+                sm.setOwner(name);
+            } else {
+                sm.setOwningPlayer(Bukkit.getOfflinePlayer(item.getUuid()));
+            }
+            List<String> lore = Arrays.asList("§7Games played: §e" + item.getGamesPlayed(),
+                    "§7W/L: §e" + StatsUtil.formatDouble(item.getWinToLoseRatio()),
+                    "§7Favourite team: §e" + StatsUtil.formatDouble(item.getTeamRatio()));
+            sm.setLore(lore);
+            sm.setDisplayName("§7" + name);
+            itemStack.setItemMeta(sm);
+            return new OrcItem(itemStack);
+        });
+
+        Map<Integer, OrcItem> extraButtons = new HashMap<>();
+
+        creator.show(player);
+    }
+}

+ 131 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/PreFetcher.java

@@ -0,0 +1,131 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.stats;
+
+import com.mojang.authlib.GameProfile;
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.inventory.OrcItem;
+import de.butzlabben.missilewars.inventory.pages.InventoryPage;
+import de.butzlabben.missilewars.inventory.pages.PageGUICreator;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.stats.StatsFetcher;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+
+public class PreFetcher {
+
+    @Getter
+    private static PrePlayerFetchRunnable runnable;
+
+    private PreFetcher() {
+    }
+
+    public static synchronized PrePlayerFetchRunnable preFetchPlayers(StatsFetcher fetcher) {
+        if (runnable != null) return runnable;
+        runnable = new PrePlayerFetchRunnable(fetcher);
+        Thread thread = new Thread(runnable);
+        thread.start();
+        return runnable;
+    }
+
+    @RequiredArgsConstructor
+    public static class PrePlayerFetchRunnable implements Runnable {
+
+        private static final int MAX_SKIN_FETCHES = 10;
+        private final StatsFetcher fetcher;
+        private boolean shouldStop = false;
+
+        @Override
+        public void run() {
+            if (!Config.isFightStatsEnabled())
+                return;
+            if (!Config.isContactAuth())
+                return;
+            List<UUID> uuids = fetcher.getPlayers();
+            Map<UUID, String> names = new HashMap<>();
+            Collections.reverse(uuids);
+            Logger.DEBUG.log("Prefetching " + uuids.size() + " player names");
+            for (UUID uuid : uuids) {
+                if (shouldStop)
+                    break;
+                try {
+                    GameProfile profile = GameProfileBuilder.fetch(uuid);
+                    names.put(uuid, profile.getName());
+                } catch (Exception e) {
+                    Logger.WARN.log("Could not prefetch player " + uuid.toString() + ". Aborting. Reason: " + e.getMessage());
+                    return;
+                }
+            }
+
+            if (!Config.isShowRealSkins()) return;
+            try {
+                Thread.sleep(6 * 10 * 1000);
+            } catch (InterruptedException ignored) {
+            }
+
+            if (shouldStop) return;
+            Logger.DEBUG.log("Prefetching " + uuids.size() + " player skins");
+            PageGUICreator<String> creator = getPreFetchCreator(names.values());
+            creator.show(null);
+            if (creator.getInvPages() == null) return;
+            int i = 0;
+            for (InventoryPage page : creator.getInvPages()) {
+                if (shouldStop) return;
+                try {
+                    Thread.sleep(20 * 1000);
+                } catch (InterruptedException ignored) {
+                }
+                if (shouldStop) return;
+                Logger.DEBUG.log("Prefetching page " + i);
+                try {
+                    page.getInventory();
+                } catch (Exception ignored) {
+                }
+                i++;
+            }
+            Logger.DEBUG.log("Players fully loaded");
+        }
+
+        public void stop() {
+            shouldStop = true;
+        }
+
+        private PageGUICreator<String> getPreFetchCreator(Collection<String> names) {
+            PageGUICreator<String> creator = new PageGUICreator<>("§ePlayer statistics", names, (item) -> {
+                ItemStack itemStack = new ItemStack(VersionUtil.getPlayerSkullMaterial());
+                SkullMeta sm = (SkullMeta) itemStack.getItemMeta();
+                //noinspection deprecation
+                sm.setOwner(item);
+                sm.setDisplayName(item);
+                itemStack.setItemMeta(sm);
+                return new OrcItem(itemStack);
+            });
+            return creator;
+        }
+    }
+}

+ 38 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/stats/StatsUtil.java

@@ -0,0 +1,38 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.stats;
+
+import java.text.DecimalFormat;
+import java.time.Duration;
+
+public class StatsUtil {
+
+    private final static DecimalFormat decimalFormat = new DecimalFormat("0.0#");
+
+    public static String formatDuration(Duration duration) {
+        long seconds = duration.getSeconds();
+        long absSeconds = Math.abs(seconds);
+        String positive = String.format("%02d:%02d", (absSeconds % 3600) / 60, absSeconds % 60);
+        return seconds < 0 ? "-" + positive : positive;
+    }
+
+    public static String formatDouble(double value) {
+        return decimalFormat.format(value);
+    }
+}

+ 32 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/BlockDataSetter.java

@@ -0,0 +1,32 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.version;
+
+import org.bukkit.block.Block;
+
+/**
+ * @author Butzlabben
+ * @since 04.09.2018
+ */
+public interface BlockDataSetter {
+
+    void setData(Block block, Object data);
+
+    void setData(Block block, Object data, boolean update);
+}

+ 114 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/BlockSetterProvider.java

@@ -0,0 +1,114 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.version;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.bukkit.block.Block;
+
+/**
+ * @author Butzlabben
+ * @since 04.09.2018
+ */
+public class BlockSetterProvider {
+
+    private static final BlockDataSetter blockSetter;
+
+    static {
+        if (VersionUtil.getVersion() < 13)
+            blockSetter = new LegacyBlockSetter();
+        else
+            blockSetter = new NewBlockSetter();
+    }
+
+    private BlockSetterProvider() {
+    }
+
+    public static BlockDataSetter getBlockDataSetter() {
+        return blockSetter;
+    }
+
+    public static Object getBlockData(Block block) {
+        try {
+            Method m = block.getClass().getMethod("getBlockData");
+            return m.invoke(block);
+        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException
+                | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static void changeBlockData(String method, Object object, Object... args) {
+        try {
+            Method m = object.getClass().getMethod(method);
+            m.invoke(object, args);
+        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException
+                | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static class LegacyBlockSetter implements BlockDataSetter {
+
+        @Override
+        public void setData(Block block, Object data) {
+            try {
+                Method m = block.getClass().getMethod("setData", byte.class);
+                m.invoke(block, data);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        @Override
+        public void setData(Block block, Object data, boolean update) {
+            try {
+                Method m = block.getClass().getMethod("setData", byte.class, boolean.class);
+                m.invoke(block, data, update);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private static class NewBlockSetter implements BlockDataSetter {
+
+        @Override
+        public void setData(Block block, Object data) {
+            try {
+                Method m = block.getClass().getMethod("setBlockData", org.bukkit.block.data.BlockData.class);
+                m.invoke(block, data);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+
+        @Override
+        public void setData(Block block, Object data, boolean update) {
+            try {
+                Method m = block.getClass().getMethod("setBlockData", org.bukkit.block.data.BlockData.class, boolean.class);
+                m.invoke(block, data, update);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 244 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/ColorConverter.java

@@ -0,0 +1,244 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.version;
+
+import org.bukkit.Color;
+import org.bukkit.Material;
+
+public class ColorConverter {
+
+    private ColorConverter() {
+    }
+
+    public static Color getColorFromCode(String s) {
+        if (s.startsWith("§")) {
+            s = s.substring(1);
+        }
+        switch (s) {
+            case "0":
+                return Color.fromRGB(0, 0, 0);
+            case "1":
+                return Color.fromRGB(0, 0, 170);
+            case "2":
+                return Color.fromRGB(0, 170, 0);
+            case "3":
+                return Color.fromRGB(0, 170, 170);
+            case "4":
+                return Color.fromRGB(170, 0, 0);
+            case "5":
+                return Color.fromRGB(170, 0, 170);
+            case "6":
+                return Color.fromRGB(255, 170, 0);
+            case "7":
+                return Color.fromRGB(170, 170, 170);
+            case "8":
+                return Color.fromRGB(85, 85, 85);
+            case "9":
+                return Color.fromRGB(85, 85, 255);
+            case "a":
+                return Color.fromRGB(85, 255, 85);
+            case "b":
+                return Color.fromRGB(85, 255, 255);
+            case "c":
+                return Color.fromRGB(255, 85, 85);
+            case "d":
+                return Color.fromRGB(255, 85, 255);
+            case "e":
+                return Color.fromRGB(255, 255, 85);
+            default:
+                return Color.WHITE;
+        }
+    }
+
+    public static Material getGlassPaneFromColorCode(String s) {
+        if (s.startsWith("§")) {
+            s = s.substring(1);
+        }
+        switch (s) {
+            case "f":
+            case "0":
+                return Material.valueOf("WHITE_STAINED_GLASS_PANE");
+            case "6":
+                return Material.valueOf("ORANGE_STAINED_GLASS_PANE");
+            case "d":
+                return Material.valueOf("PINK_STAINED_GLASS_PANE");
+            case "b":
+                return Material.valueOf("CYAN_STAINED_GLASS_PANE");
+            case "e":
+                return Material.valueOf("YELLOW_STAINED_GLASS_PANE");
+            case "a":
+            case "2":
+                return Material.valueOf("GREEN_STAINED_GLASS_PANE");
+            case "8":
+                return Material.valueOf("GRAY_STAINED_GLASS_PANE");
+            case "7":
+                return Material.valueOf("LIGHT_GRAY_STAINED_GLASS_PANE");
+            case "3":
+                return Material.valueOf("LIGHT_BLUE_STAINED_GLASS_PANE");
+            case "5":
+            case "1":
+                return Material.valueOf("MAGENTA_STAINED_GLASS_PANE");
+            case "9":
+                return Material.valueOf("BLUE_STAINED_GLASS_PANE");
+            case "c":
+            case "4":
+                return Material.valueOf("RED_STAINED_GLASS_PANE");
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * @param i from colored Blocks for example Wool
+     *
+     * @return Color Code in Form "§f"
+     *
+     * @throws IllegalArgumentException if i is < 0 or > 15
+     * @author Butzlabben
+     * @since 26.09.2017
+     */
+    public static String getLegacyColorCodefromBlock(int i) {
+        if (i < 0 || i > 15) {
+            throw new IllegalArgumentException();
+        }
+        switch (i) {
+            case 0:
+                return "§f";
+            case 1:
+                return "§6";
+            case 2:
+                return "§d";
+            case 3:
+                return "§b";
+            case 4:
+                return "§e";
+            case 5:
+                return "§a";
+            case 6:
+                return "§c";
+            case 7:
+                return "§8";
+            case 8:
+                return "§7";
+            case 9:
+                return "§3";
+            case 10:
+                return "§5";
+            case 11:
+                return "§1";
+            case 12:
+                return "";
+            case 13:
+                return "§2";
+            case 14:
+                return "§4";
+            case 15:
+                return "§0";
+        }
+        return null;
+    }
+
+    /**
+     * @param s Code in Form "§f"
+     *
+     * @return SubID from colored Blocks for example Wool
+     *
+     * @throws IllegalArgumentException if there is no ColorCode found
+     * @author Butzlabben
+     * @since 26.09.2017
+     */
+    public static byte getColorIDforBlockFromColorCode(String s) {
+        if (s.startsWith("§")) {
+            s = s.substring(1);
+        }
+        switch (s) {
+            case "f":
+                return 0;
+            case "6":
+                return 1;
+            case "d":
+                return 2;
+            case "b":
+                return 3;
+            case "e":
+                return 4;
+            case "a":
+                return 5;
+            case "8":
+                return 7;
+            case "7":
+                return 8;
+            case "3":
+                return 9;
+            case "5":
+                return 10;
+            case "1":
+            case "9":
+                return 11;
+            case "2":
+                return 12;
+            case "c":
+            case "4":
+                return 14;
+            case "0":
+                return 15;
+            default:
+                throw new IllegalArgumentException();
+
+        }
+    }
+
+    public static Material getGlassFromColorCode(String s) {
+        if (s.startsWith("§")) {
+            s = s.substring(1);
+        }
+        switch (s) {
+            case "f":
+            case "0":
+                return Material.valueOf("WHITE_STAINED_GLASS");
+            case "6":
+                return Material.valueOf("ORANGE_STAINED_GLASS");
+            case "d":
+                return Material.valueOf("PINK_STAINED_GLASS");
+            case "b":
+                return Material.valueOf("CYAN_STAINED_GLASS");
+            case "e":
+                return Material.valueOf("YELLOW_STAINED_GLASS");
+            case "a":
+            case "2":
+                return Material.valueOf("GREEN_STAINED_GLASS");
+            case "8":
+                return Material.valueOf("GRAY_STAINED_GLASS");
+            case "7":
+                return Material.valueOf("LIGHT_GRAY_STAINED_GLASS");
+            case "3":
+                return Material.valueOf("LIGHT_BLUE_STAINED_GLASS");
+            case "5":
+            case "1":
+                return Material.valueOf("MAGENTA_STAINED_GLASS");
+            case "9":
+                return Material.valueOf("BLUE_STAINED_GLASS");
+            case "c":
+            case "4":
+                return Material.valueOf("RED_STAINED_GLASS");
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+}

+ 270 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/util/version/VersionUtil.java

@@ -0,0 +1,270 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.util.version;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.lang.reflect.Method;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.Sound;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+/**
+ * @author Butzlabben
+ * @since 14.08.2018
+ */
+public class VersionUtil {
+
+    private static int version;
+
+    private VersionUtil() {
+    }
+
+    public static void setUnbreakable(ItemStack is) {
+        ItemMeta im = is.getItemMeta();
+        if (getVersion() >= 11) {
+            im.setUnbreakable(true);
+        } else {
+//            im.spigot().setUnbreakable(true);
+        }
+        is.setItemMeta(im);
+    }
+
+    public static void playFireball(Player p, Location loc) {
+        if (getVersion() <= 8) {
+            p.playSound(loc, sound("ANVIL_LAND"), 100.0F, 2.0F);
+            p.playSound(loc, sound("FIRE_IGNITE"), 100.0F, 1.0F);
+        } else {
+            p.playSound(loc, sound("BLOCK_ANVIL_LAND"), 100.0F, 2.0F);
+            p.playSound(loc, sound("ITEM_FLINTANDSTEEL_USE"), 100.0F, 1.0F);
+        }
+    }
+
+    public static void playSnowball(Player p, Location loc) {
+        if (getVersion() <= 8)
+            p.playSound(loc, sound("ENDERDRAGON_WINGS"), 1.0F, 1.0F);
+        else if (getVersion() <= 12)
+            p.playSound(loc, sound("ENTITY_ENDERDRAGON_FLAP"), 1, 1);
+        else
+            p.playSound(loc, sound("ENTITY_ENDER_DRAGON_FLAP"), 1, 1);
+    }
+
+    public static void playPling(Player p) {
+        if (getVersion() <= 8)
+            p.playSound(p.getLocation(), sound("NOTE_PLING"), 100, 3);
+        else if (getVersion() >= 13)
+            p.playSound(p.getLocation(), sound("BLOCK_NOTE_BLOCK_PLING"), 100, 3);
+        else
+            p.playSound(p.getLocation(), sound("BLOCK_NOTE_PLING"), 100, 3);
+    }
+
+    public static void playDraw(Player p) {
+        if (getVersion() <= 8)
+            p.playSound(p.getLocation(), sound("WITHER_DEATH"), 100, 0);
+        else
+            p.playSound(p.getLocation(), sound("ENTITY_WITHER_DEATH"), 100, 0);
+    }
+
+    public static void restart() {
+        Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
+    }
+
+    public static void sendTitle(Player p, String title, String subtitle) {
+        if (getVersion() > 8) {
+            try {
+                Method m = p.getClass().getMethod("sendTitle", String.class, String.class);
+                m.invoke(p, title, subtitle);
+            } catch (Exception e) {
+                Logger.ERROR.log("Couldn't send title to player");
+                e.printStackTrace();
+            }
+        } else {
+            p.sendMessage(MessageConfig.getPrefix() + title + " " + subtitle);
+        }
+    }
+
+    public static void setScoreboardTeamColor(org.bukkit.scoreboard.Team team, ChatColor color) {
+        if (VersionUtil.getVersion() >= 12) {
+            team.setColor(color);
+        }
+    }
+
+    public static Material getFireball() {
+        if (getVersion() < 13)
+            return Material.valueOf("FIREBALL");
+        else
+            return Material.valueOf("FIRE_CHARGE");
+    }
+
+    public static Material getSnowball() {
+        if (getVersion() < 13)
+            return Material.valueOf("SNOW_BALL");
+        else
+            return Material.valueOf("SNOWBALL");
+    }
+
+    public static Material getMonsterEgg(EntityType type) {
+        if (getVersion() < 13)
+            return Material.valueOf("MONSTER_EGG");
+        else {
+            if (type == EntityType.MUSHROOM_COW) {
+                //noinspection SpellCheckingInspection
+                return Material.valueOf("MOOSHROOM_SPAWN_EGG");
+
+            }
+            return Material.valueOf(type.name() + "_SPAWN_EGG");
+        }
+    }
+
+    public static Material getPortal() {
+        if (getVersion() < 13)
+            return Material.valueOf("PORTAL");
+        else
+            return Material.valueOf("NETHER_PORTAL");
+    }
+
+    public static Material getSunFlower() {
+        if (getVersion() > 12)
+            return Material.valueOf("SUNFLOWER");
+        else
+            return Material.valueOf("DOUBLE_PLANT");
+    }
+
+    public static int getVersion() {
+        if (version == 0) {
+            // Detect version
+            String v = Bukkit.getVersion();
+            if (v.contains("1.20"))
+                version = 20;
+            else if (v.contains("1.19"))
+                version = 19;
+            else if (v.contains("1.18"))
+                version = 18;
+            else if (v.contains("1.17"))
+                version = 17;
+            else if (v.contains("1.16"))
+                version = 16;
+            else if (v.contains("1.15"))
+                version = 15;
+            else if (v.contains("1.14"))
+                version = 14;
+            else if (v.contains("1.13"))
+                version = 13;
+            else if (v.contains("1.12"))
+                version = 12;
+            else if (v.contains("1.11"))
+                version = 11;
+            else if (v.contains("1.10"))
+                version = 10;
+            else if (v.contains("1.9"))
+                version = 9;
+            else if (v.contains("1.8"))
+                version = 8;
+            else if (v.contains("1.7"))
+                version = 7;
+            else if (v.contains("1.6"))
+                version = 6;
+            else if (v.contains("1.5"))
+                version = 5;
+            else if (v.contains("1.4"))
+                version = 4;
+            else if (v.contains("1.3"))
+                version = 3;
+        }
+        if (version == 0) {
+            Logger.WARN.log("Unknown version: " + Bukkit.getVersion());
+            Logger.WARN.log("Choosing version 1.12.2");
+            version = 12;
+        }
+        return version;
+    }
+
+    private static Sound sound(String s) {
+        Sound sound = null;
+        try {
+            sound = Sound.valueOf(s);
+        } catch (Exception e) {
+            Logger.ERROR.log("Couldn't find sound " + s);
+        }
+        return sound;
+    }
+
+    public static boolean isStainedGlassPane(Material material) {
+        if (material == null)
+            return false;
+        return material.name().contains("STAINED_GLASS_PANE");
+    }
+
+    public static boolean isMonsterEgg(Material material) {
+        if (material == null)
+            return false;
+        String name = material.name();
+        if (name.equals("EGG"))
+            return false;
+        if (name.contains("SPAWN_EGG"))
+            return true;
+        return name.equals("MONSTER_EGG");
+    }
+
+    @SuppressWarnings("deprecation")
+    public static ItemStack getGlassPlane(Team team) {
+        String colorCode = team.getColorCode();
+        ItemStack is;
+        if (getVersion() < 13) {
+            is = new ItemStack(Material.valueOf("STAINED_GLASS_PANE"), 1, ColorConverter.getColorIDforBlockFromColorCode(colorCode));
+        } else {
+            is = new ItemStack(ColorConverter.getGlassPaneFromColorCode(colorCode));
+        }
+
+        ItemMeta im = is.getItemMeta();
+        im.setDisplayName(team.getFullname());
+        is.setItemMeta(im);
+        return is;
+    }
+
+    @SuppressWarnings("deprecation")
+    public static ItemStack getGlassPlane(String colorCode) {
+        ItemStack is;
+        if (getVersion() < 13) {
+            is = new ItemStack(Material.valueOf("STAINED_GLASS_PANE"), 1, ColorConverter.getColorIDforBlockFromColorCode(colorCode));
+        } else {
+            is = new ItemStack(ColorConverter.getGlassPaneFromColorCode(colorCode));
+        }
+        return is;
+    }
+
+    public static Material getPlayerSkullMaterial() {
+        if (getVersion() > 12) {
+            return Material.valueOf("PLAYER_HEAD");
+        } else {
+            return Material.valueOf("SKULL_ITEM");
+        }
+    }
+
+    public static boolean isWallSignMaterial(Material material) {
+        return material.name().contains("WALL_SIGN");
+    }
+}

+ 102 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/Arena.java

@@ -0,0 +1,102 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts;
+
+import com.google.gson.annotations.SerializedName;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.FallProtectionConfiguration;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.FireballConfiguration;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.MissileConfiguration;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.MoneyConfiguration;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.ShieldConfiguration;
+import de.butzlabben.missilewars.wrapper.geometry.FlatArea;
+import de.butzlabben.missilewars.wrapper.geometry.Plane;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.bukkit.Location;
+import org.bukkit.util.Vector;
+
+@Data
+@ToString
+@AllArgsConstructor
+@RequiredArgsConstructor
+@Builder(toBuilder = true)
+public class Arena {
+
+    private String name = "arena";
+    @SerializedName("display_name") private String displayName = "&eDefault map";
+    @SerializedName("display_material") private String displayMaterial = "STONE";
+    @SerializedName("template_world") private String templateWorld = "default_map";
+    @SerializedName("auto_respawn") private boolean autoRespawn = true;
+    @SerializedName("do_tile_drops") private boolean doTileDrops = false;
+    @SerializedName("max_height") private int maxHeight = 170;
+    @SerializedName("death_height") private int deathHeight = 65;
+    @SerializedName("max_spectators") private int maxSpectators = -1;
+    @SerializedName("game_duration") private int gameDuration = 30;
+    @SerializedName("fireball") private FireballConfiguration fireballConfiguration = new FireballConfiguration();
+    @SerializedName("shield") private ShieldConfiguration shieldConfiguration = new ShieldConfiguration();
+    @SerializedName("arrow_occurrence") private int arrowOccurrence = 2;
+    @SerializedName("save_statistics") private boolean saveStatistics = true;
+    @SerializedName("fall_protection") private FallProtectionConfiguration fallProtection = new FallProtectionConfiguration();
+    @SerializedName("money") private MoneyConfiguration money = new MoneyConfiguration();
+    @SerializedName("intervals") private Map<Integer, Integer> intervals = new HashMap<Integer, Integer>() {{
+        put(1, 15);
+        put(2, 20);
+        put(4, 25);
+    }};
+    @SerializedName("missile_configuration") private MissileConfiguration missileConfiguration = new MissileConfiguration();
+    @SerializedName("spectator_spawn") private Location spectatorSpawn = new Location(null, 0, 100, 0, 90, 0);
+    @SerializedName("area") private FlatArea gameArea = new FlatArea(-30, -72, 30, 72);
+    @SerializedName("team1_spawn") private Location team1Spawn = new Location(null, 0.5, 100, 45.5, 180, 0);
+    @SerializedName("team2_spawn") private Location team2Spawn = new Location(null, 0.5, 100, -45.5, 0, 0);
+
+    public int getInterval(int teamSize) {
+        if (intervals.isEmpty()) {
+            Logger.WARN.log("The given interval mapping in \"" + name + "\" is empty. Choosing default value 20");
+            return 20;
+        }
+        if (intervals.containsKey(teamSize)) return intervals.get(teamSize);
+        for (int i = teamSize; i > 0; i--) {
+            if (intervals.containsKey(i)) return intervals.get(i);
+        }
+        int highestMapping = Collections.max(intervals.keySet());
+        for (int i = teamSize; i < highestMapping; i++) {
+            if (intervals.containsKey(i)) return intervals.get(i);
+        }
+        throw new IllegalStateException("We should never arrive here, ...");
+    }
+
+    public Plane getPlane1() {
+        Vector spawn1 = team1Spawn.toVector();
+        Vector normal = team2Spawn.toVector().subtract(spawn1);
+        return new Plane(spawn1, normal);
+    }
+
+    public Plane getPlane2() {
+        Vector spawn2 = team2Spawn.toVector();
+        Vector normal = team1Spawn.toVector().subtract(spawn2);
+        return new Plane(spawn2, normal);
+    }
+}

+ 139 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/GameWorld.java

@@ -0,0 +1,139 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.game.Game;
+import java.io.File;
+import java.io.IOException;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.io.FileUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.WorldCreator;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+
+@Getter
+@ToString(exclude = {"game", "lock"})
+public class GameWorld {
+
+    private final String templateName;
+    private final String worldNameTemplate;
+    private final Game game;
+    private final Object lock = new Object();
+    private String worldName;
+
+    public GameWorld(Game game, String templateName) {
+        this.templateName = templateName;
+        this.game = game;
+        this.worldNameTemplate = "mw-" + templateName;
+    }
+
+    public boolean isWorld(World world) {
+        if (world == null)
+            return false;
+        return world.getName().equals(worldName);
+    }
+
+    public World getWorld() {
+        return Bukkit.getWorld(worldName);
+    }
+
+    public void kickInactivity() {
+        synchronized (lock) {
+            Bukkit.getOnlinePlayers().forEach(p -> {
+                if (p.isDead() && p.getWorld().getName().equals(worldName)) {
+                    p.kickPlayer(MessageConfig.getMessage("kick_inactivity"));
+                }
+            });
+        }
+    }
+
+    public void sendPlayersBack() {
+        synchronized (lock) {
+            World w = Bukkit.getWorld(worldName);
+            if (w == null)
+                return;
+            Lobby lobby = game.getLobby();
+            w.getEntities().stream().filter((f) -> f instanceof Player).forEach(p -> p.teleport(lobby.getAfterGameSpawn()));
+        }
+    }
+
+    public void unload() {
+        synchronized (lock) {
+            World w = Bukkit.getWorld(worldName);
+            if (w == null)
+                return;
+            Logger.DEBUG.log("Unloading old world");
+            for (Entity e : w.getEntities()) {
+                if (e instanceof Player) {
+//                    e.remove();
+                    Logger.DEBUG.log("Removing: " + e.getName());
+                }
+            }
+            Bukkit.getWorlds().remove(w);
+            Bukkit.unloadWorld(w, false);
+        }
+    }
+
+    public void delete() {
+        synchronized (lock) {
+            Logger.DEBUG.log("Deleting old world");
+            File file = new File(worldName);
+            FileUtils.deleteQuietly(file);
+            if (file.exists() || file.isDirectory()) {
+                Logger.WARN.log("Could not delete old world!");
+                file.delete();
+            }
+        }
+    }
+
+    public void load() {
+        synchronized (lock) {
+            int i = 0;
+            File file;
+            do {
+                worldName = worldNameTemplate + "-" + i;
+                file = new File(Bukkit.getWorldContainer(), worldName);
+                i++;
+            } while (file.exists() || file.isDirectory());
+
+            File newFile = new File(Config.getArenaFolder() + "/" + templateName);
+
+            try {
+                FileUtils.copyDirectory(newFile, file);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+
+            File uid = new File(file, "uid.dat");
+            if (uid.isFile()) FileUtils.deleteQuietly(uid);
+
+            Logger.DEBUG.log("Loading new gameworld");
+            World world = Bukkit.createWorld(new WorldCreator(worldName));
+            Bukkit.getWorlds().add(world);
+            world.setGameRuleValue("doTileDrops", String.valueOf(game.getArena().isDoTileDrops()));
+        }
+    }
+
+}

+ 92 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/Lobby.java

@@ -0,0 +1,92 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts;
+
+import com.google.gson.annotations.SerializedName;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.wrapper.geometry.Area;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+
+@RequiredArgsConstructor
+@Getter
+@ToString
+@AllArgsConstructor
+public class Lobby {
+
+    private String name = "lobby0";
+    @SerializedName("display_name") private String displayName = "&eDefault game";
+    @SerializedName("world") private String world = Bukkit.getWorlds().get(0).getName();
+    @SerializedName("lobby_time") private int lobbyTime = 60;
+    @SerializedName("join_ongoing_game") private boolean joinOngoingGame = false;
+    @SerializedName("min_size") private int minSize = 2;
+    @SerializedName("max_size") private int maxSize = 20;
+    @SerializedName("team1_name") private String team1Name = "Team1";
+    @SerializedName("team1_color") private String team1Color = "&c";
+    @SerializedName("team2_name") private String team2Name = "Team2";
+    @SerializedName("team2_color") private String team2Color = "&a";
+    @SerializedName("spawn_point") private Location spawnPoint = Bukkit.getWorlds().get(0).getSpawnLocation();
+    @SerializedName("after_game_spawn") private Location afterGameSpawn = Bukkit.getWorlds().get(0).getSpawnLocation();
+    private Area area = Area.defaultAreaAround(Bukkit.getWorlds().get(0).getSpawnLocation());
+    @SerializedName("map_choose_procedure") private MapChooseProcedure mapChooseProcedure = MapChooseProcedure.FIRST;
+    @SerializedName("possible_arenas") private List<String> possibleArenas = new ArrayList<String>() {{
+        add("arena");
+    }};
+
+    @Setter private transient File file;
+
+    public World getBukkitWorld() {
+        World world = Bukkit.getWorld(getWorld());
+        if (world == null) {
+            Logger.ERROR.log("Could not find any world with the name: " + this.world);
+            Logger.ERROR.log("Please correct this in the configuration of lobby \"" + name + "\"");
+        }
+        return world;
+    }
+
+    public void checkForWrongArenas() {
+        for (String arenaName : possibleArenas) {
+            Optional<Arena> arena = Arenas.getFromName(arenaName);
+            if (!arena.isPresent()) {
+                Logger.WARN.log("Could not find arena with name \"" + arenaName + "\" for lobby \"" + getName() + "\"");
+            }
+        }
+    }
+
+    public List<Arena> getArenas() {
+        return possibleArenas
+                .stream()
+                .map(Arenas::getFromName)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+    }
+}

+ 23 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/MapChooseProcedure.java

@@ -0,0 +1,23 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts;
+
+public enum MapChooseProcedure {
+    FIRST, MAPCYCLE, MAPVOTING
+}

+ 33 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/FallProtectionConfiguration.java

@@ -0,0 +1,33 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts.arena;
+
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+@Getter
+@ToString
+@RequiredArgsConstructor
+public class FallProtectionConfiguration {
+
+    private final boolean enabled = true;
+    private final int duration = 60;
+}

+ 34 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/FireballConfiguration.java

@@ -0,0 +1,34 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts.arena;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+@Getter
+@ToString
+@RequiredArgsConstructor
+public class FireballConfiguration {
+
+    private final String name = "&cFireball";
+    private final int occurrence = 2;
+    @SerializedName("destroy_portal") private boolean destroysPortal = false;
+}

+ 102 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/MissileConfiguration.java

@@ -0,0 +1,102 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts.arena;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.wrapper.missile.Missile;
+import de.butzlabben.missilewars.wrapper.missile.MissileFacing;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.bukkit.entity.EntityType;
+
+
+@Getter
+@RequiredArgsConstructor
+@ToString
+public class MissileConfiguration {
+
+    // TODO pretty names
+
+    private final boolean onlyBlockPlaceable = false;
+    private final boolean onlyBetweenSpawnPlaceable = false;
+    private final boolean northFacing = true;
+    private final boolean eastFacing = true;
+    private final boolean southFacing = true;
+    private final boolean westFacing = true;
+    private final List<Missile> missiles = new ArrayList<Missile>() {{
+        add(new Missile("Tomahawk.schematic", "&aTomahawk", EntityType.CREEPER, 2, 2, 3));
+        add(new Missile("Cruiser.schematic", "&eCruiser", EntityType.BLAZE, 2, 2, 2));
+        add(new Missile("Sword.schematic", "&7Sword", EntityType.SKELETON, 2, 2, 2));
+        add(new Missile("Juggernaut.schematic", "&4Juggernaut", EntityType.MUSHROOM_COW, 2, 2, 1));
+        add(new Missile("Piranha.schematic", "&3Piranha", EntityType.HORSE, 2, 2, 3));
+        add(new Missile("Tunnelbore.schematic", "&0Tunnelbore", EntityType.ENDERMAN, 2, 2, 1));
+    }};
+
+    public List<MissileFacing> getEnabledFacings() {
+        List<MissileFacing> enabledDirections = new ArrayList<>();
+        if (northFacing) enabledDirections.add(MissileFacing.NORTH);
+        if (eastFacing) enabledDirections.add(MissileFacing.EAST);
+        if (southFacing) enabledDirections.add(MissileFacing.SOUTH);
+        if (westFacing) enabledDirections.add(MissileFacing.WEST);
+        if (enabledDirections.isEmpty()) {
+            Logger.WARN.log("All facings were disabled for an arena. Please correct this issue");
+            enabledDirections.addAll(Arrays.asList(MissileFacing.values()));
+        }
+        return enabledDirections;
+    }
+
+    public Missile getMissileFromName(String name) {
+        for (Missile m : missiles) {
+            if (m.getName().equalsIgnoreCase(name) || m.getName().replaceAll("§.", "").equalsIgnoreCase(name))
+                return m;
+        }
+        return null;
+    }
+
+    public Missile getMissileFromType(EntityType type) {
+        for (Missile m : missiles) {
+            if (m.getType() == type)
+                return m;
+        }
+        return null;
+    }
+
+    public Missile getMissileFromID(int i) {
+        return missiles.get(i);
+    }
+
+    public void check() {
+        Set<Missile> toRemove = new HashSet<>();
+        for (Missile missile : missiles) {
+            File schematic = missile.getSchematic();
+            if (!schematic.exists()) {
+                Logger.WARN.log(missile.getName() + " §7has no schematic. Removing this missile");
+                toRemove.add(missile);
+            }
+        }
+        missiles.removeAll(toRemove);
+    }
+}

+ 33 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/MoneyConfiguration.java

@@ -0,0 +1,33 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts.arena;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+@Getter
+@ToString
+@RequiredArgsConstructor
+public class MoneyConfiguration {
+
+    private final int win = 80;
+    private final int loss = 50;
+    private final int draw = 30;
+}

+ 35 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/abstracts/arena/ShieldConfiguration.java

@@ -0,0 +1,35 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.abstracts.arena;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+@Getter
+@ToString
+@RequiredArgsConstructor
+public class ShieldConfiguration {
+
+    private final String name = "Shield";
+    private final String schematic = "shield.schematic";
+    private final int occurrence = 1;
+    @SerializedName("serialized_name") private int flyTime = 20;
+}

+ 46 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/GameEndEvent.java

@@ -0,0 +1,46 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.event;
+
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.util.Optional;
+import lombok.Getter;
+import org.bukkit.event.HandlerList;
+
+@Getter
+public class GameEndEvent extends GameEvent {
+
+    public final static HandlerList handlers = new HandlerList();
+    private final Optional<Team> winningTeam;
+
+    public GameEndEvent(Game game, Team winningTeam) {
+        super(game);
+        this.winningTeam = Optional.ofNullable(winningTeam);
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return handlers;
+    }
+}

+ 31 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/GameEvent.java

@@ -0,0 +1,31 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.event;
+
+import de.butzlabben.missilewars.game.Game;
+import org.bukkit.event.Event;
+
+public abstract class GameEvent extends Event {
+
+    private final Game game;
+
+    public GameEvent(Game game) {
+        this.game = game;
+    }
+}

+ 40 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/GameStartEvent.java

@@ -0,0 +1,40 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.event;
+
+import de.butzlabben.missilewars.game.Game;
+import org.bukkit.event.HandlerList;
+
+public class GameStartEvent extends GameEvent {
+
+    public final static HandlerList handlers = new HandlerList();
+
+    public GameStartEvent(Game game) {
+        super(game);
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return handlers;
+    }
+}

+ 49 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/PlayerArenaJoinEvent.java

@@ -0,0 +1,49 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.event;
+
+import de.butzlabben.missilewars.game.Game;
+import lombok.Getter;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.PlayerEvent;
+
+/**
+ * Get's called, when a player has already entered an arena
+ */
+@Getter
+public class PlayerArenaJoinEvent extends PlayerEvent {
+
+    public final static HandlerList handlers = new HandlerList();
+    private final Game game;
+
+    public PlayerArenaJoinEvent(Player who, Game game) {
+        super(who);
+        this.game = game;
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return handlers;
+    }
+}

+ 49 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/PlayerArenaLeaveEvent.java

@@ -0,0 +1,49 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.event;
+
+import de.butzlabben.missilewars.game.Game;
+import lombok.Getter;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.PlayerEvent;
+
+/**
+ * Get's called, when a player has already left an arena
+ */
+@Getter
+public class PlayerArenaLeaveEvent extends PlayerEvent {
+
+    public final static HandlerList handlers = new HandlerList();
+    private final Game game;
+
+    public PlayerArenaLeaveEvent(Player who, Game game) {
+        super(who);
+        this.game = game;
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return handlers;
+    }
+}

+ 61 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/event/PrePlayerArenaJoinEvent.java

@@ -0,0 +1,61 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.event;
+
+import de.butzlabben.missilewars.game.Game;
+import lombok.Getter;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.PlayerEvent;
+
+/**
+ * Get's called, before a player want's to join an arena
+ */
+@Getter
+public class PrePlayerArenaJoinEvent extends PlayerEvent implements Cancellable {
+
+    public final static HandlerList handlers = new HandlerList();
+    private final Game game;
+    private boolean cancelled;
+
+    public PrePlayerArenaJoinEvent(Player who, Game game) {
+        super(who);
+        this.game = game;
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return handlers;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return cancelled;
+    }
+
+    @Override
+    public void setCancelled(boolean b) {
+        cancelled = b;
+    }
+}

+ 139 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/game/RespawnGoldBlock.java

@@ -0,0 +1,139 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.game;
+
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameState;
+import de.butzlabben.missilewars.util.version.BlockSetterProvider;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.Map;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerToggleSneakEvent;
+
+/**
+ * @author Butzlabben
+ * @since 14.01.2018
+ */
+public class RespawnGoldBlock implements Listener {
+
+    private final Player player;
+    private final Game game;
+    private final Map<Location, Map.Entry<Material, ?>> map = new HashMap<>();
+    private int duration;
+    private int task;
+
+    public RespawnGoldBlock(Player p, int dur, Game game) {
+        this.duration = dur;
+        this.player = p;
+        this.game = game;
+        Bukkit.getPluginManager().registerEvents(this, MissileWars.getInstance());
+        activate();
+    }
+
+    private void activate() {
+        task = Bukkit.getScheduler().scheduleSyncRepeatingTask(MissileWars.getInstance(), () -> {
+            if (duration == 0) {
+                stop();
+                return;
+            }
+            double seconds = (double) duration / 20;
+            if ((seconds == Math.floor(seconds)) && !Double.isInfinite(seconds)) {
+                player.sendMessage(MessageConfig.getMessage("fall_protection").replace("%seconds%", "" + (int) seconds));
+            }
+            if (player.getGameMode() != GameMode.SURVIVAL) {
+                stop();
+                return;
+            }
+            if (game.getState() != GameState.INGAME) {
+                stop();
+                return;
+            }
+            for (Location loc : map.keySet()) {
+                loc.getBlock().setType(map.get(loc).getKey());
+                BlockSetterProvider.getBlockDataSetter().setData(loc.getBlock(), map.get(loc).getValue());
+            }
+            map.clear();
+            Location curr = player.getLocation().clone();
+            curr.setY(curr.getY() - 1.0D);
+            Block b = curr.getBlock();
+            setBlock(b);
+            curr = curr.clone().add(-1.0D, 0.0D, 0.0D);
+            b = curr.getBlock();
+            setBlock(b);
+            curr = curr.clone().add(2.0D, 0.0D, 0.0D);
+            b = curr.getBlock();
+            setBlock(b);
+            curr = curr.clone().add(-1.0D, 0.0D, -1.0D);
+            b = curr.getBlock();
+            setBlock(b);
+            curr = curr.clone().add(0.0D, 0.0D, 2.0D);
+            b = curr.getBlock();
+            setBlock(b);
+            --duration;
+        }, 0L, 1L);
+    }
+
+    private void setBlock(Block b) {
+        if ((b.getType() != Material.GOLD_BLOCK) && (b.getType() == Material.AIR)) {
+            Object data = b.getData();
+            if (VersionUtil.getVersion() >= 13)
+                data = b.getBlockData();
+            map.put(b.getLocation(), new AbstractMap.SimpleEntry<>(b.getType(), data));
+            b.setType(Material.GOLD_BLOCK);
+        }
+    }
+
+    public void stop() {
+        for (Location loc : map.keySet()) {
+            loc.getBlock().setType(map.get(loc).getKey());
+            BlockSetterProvider.getBlockDataSetter().setData(loc.getBlock(), map.get(loc).getValue());
+        }
+        map.clear();
+        player.sendMessage(MessageConfig.getMessage("fall_protection_inactive"));
+        Bukkit.getScheduler().cancelTask(task);
+        HandlerList.unregisterAll(this);
+    }
+
+    @EventHandler
+    public void onSneak(PlayerToggleSneakEvent e) {
+        Player p = e.getPlayer();
+        if (p == player && (map.size() != 0) && (p.isSneaking())) {
+            for (Location loc : map.keySet()) {
+                loc.getBlock().setType(map.get(loc).getKey());
+                BlockSetterProvider.getBlockDataSetter().setData(loc.getBlock(), map.get(loc).getValue());
+            }
+            map.clear();
+            Bukkit.getScheduler().cancelTask(task);
+            HandlerList.unregisterAll(this);
+            p.sendMessage(MessageConfig.getMessage("fall_protection_deactivated"));
+        }
+    }
+}

+ 93 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/game/Shield.java

@@ -0,0 +1,93 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.game;
+
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.ShieldConfiguration;
+import de.butzlabben.missilewars.wrapper.missile.paste.PasteProvider;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.io.IOUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.ProjectileHitEvent;
+import org.bukkit.event.entity.ProjectileLaunchEvent;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Butzlabben
+ * @since 11.09.2018
+ */
+@RequiredArgsConstructor
+public class Shield implements Listener {
+
+    private final Player player;
+    private final ShieldConfiguration shieldConfiguration;
+    private org.bukkit.entity.Snowball ball;
+
+    public static String getContent(String uri) throws IOException {
+        URL url = new URL(uri);
+        URLConnection con = url.openConnection();
+        con.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2");
+        InputStream in = con.getInputStream();
+        String encoding = con.getContentEncoding();
+        encoding = encoding == null ? "UTF-8" : encoding;
+        return IOUtils.toString(in, encoding);
+    }
+
+    public void onThrow(ProjectileLaunchEvent e) {
+        ball = (org.bukkit.entity.Snowball) e.getEntity();
+        Bukkit.getPluginManager().registerEvents(this, MissileWars.getInstance());
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> {
+            try {
+                if (!ball.isDead())
+                    paste();
+                HandlerList.unregisterAll(this);
+            } catch (Exception e1) {
+                e1.printStackTrace();
+            }
+        }, shieldConfiguration.getFlyTime());
+    }
+
+    @EventHandler
+    public void onHit(ProjectileHitEvent e) {
+        if (e.getEntity().equals(ball) || ball == e.getEntity()) {
+            HandlerList.unregisterAll(this);
+            paste();
+        }
+    }
+
+    public void paste() {
+        Location loc = ball.getLocation();
+        Vector pastePos = new Vector(loc.getX(), loc.getY(), loc.getZ());
+        File schem = new File(MissileWars.getInstance().getDataFolder(), shieldConfiguration.getSchematic());
+
+        PasteProvider.getPaster().pasteSchematic(schem, pastePos, loc.getWorld());
+        VersionUtil.playSnowball(player, player.getLocation());
+    }
+}

+ 201 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/game/Team.java

@@ -0,0 +1,201 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.game;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.MoneyUtil;
+import de.butzlabben.missilewars.util.version.ColorConverter;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.event.GameEndEvent;
+import de.butzlabben.missilewars.wrapper.player.MWPlayer;
+import java.util.ArrayList;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import org.bukkit.Bukkit;
+import org.bukkit.Color;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.LeatherArmorMeta;
+import org.bukkit.scoreboard.Scoreboard;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+
+@RequiredArgsConstructor
+@ToString(of = {"name", "members"})
+@Getter
+public class Team {
+
+    private final String name;
+    private final String color;
+    private final Game game;
+    private final transient ArrayList<MWPlayer> members = new ArrayList<>();
+    @Setter private Location spawn;
+    private transient boolean won;
+    private transient org.bukkit.scoreboard.Team scoreboardTeam;
+    private transient int currentInterval = 0;
+
+    public ArrayList<MWPlayer> getMembers() {
+        return members;
+    }
+
+    public Team getEnemyTeam() {
+        if (this == game.getTeam1())
+            return game.getTeam2();
+        return game.getTeam1();
+    }
+
+    @SuppressWarnings("deprecation")
+    public boolean removeMember(MWPlayer player) {
+        if (!isMember(player))
+            return false;
+
+        Player p = player.getPlayer();
+        player.setTeam(null);
+        if (p != null) {
+            if (scoreboardTeam.hasPlayer(p))
+                scoreboardTeam.removePlayer(p);
+
+            game.getScoreboard().getTeam("2Guest§7").addPlayer(p);
+            p.setDisplayName("§7" + p.getName() + "§r");
+        }
+        return members.removeIf(mp -> mp.getUUID().equals(player.getUUID()));
+    }
+
+    @SuppressWarnings("deprecation")
+    public void addMember(MWPlayer player) {
+        if (isMember(player))
+            return;
+        if (player.getTeam() != null) {
+            player.getTeam().removeMember(player);
+        }
+        Player p = player.getPlayer();
+        if (p == null) {
+            Logger.WARN.log("Could not add player " + player.getUUID().toString() + " to a team because he went offline");
+            return;
+        }
+        members.add(player);
+        player.setTeam(this);
+        p.setDisplayName(getColorCode() + p.getName() + "§r");
+        Scoreboard sb = game.getScoreboard();
+        if (sb.getPlayerTeam(p) != null)
+            sb.getPlayerTeam(p).removePlayer(p);
+        scoreboardTeam.addPlayer(p);
+        setTeamArmor(p);
+    }
+
+    public org.bukkit.scoreboard.Team getSBTeam() {
+        return scoreboardTeam;
+    }
+
+    public void setSBTeam(org.bukkit.scoreboard.Team team) {
+        scoreboardTeam = team;
+    }
+
+    public String getFullname() {
+        return getColorCode() + name;
+    }
+
+    public String getColorCode() {
+        if (!color.startsWith("§"))
+            return "§" + color;
+        return color;
+    }
+
+    public void setTeamArmor(Player p) {
+        Color c = ColorConverter.getColorFromCode(getColorCode());
+        ItemStack is = new ItemStack(Material.LEATHER_BOOTS);
+        LeatherArmorMeta lam = (LeatherArmorMeta) is.getItemMeta();
+        lam.setColor(c);
+        is.setItemMeta(lam);
+        VersionUtil.setUnbreakable(is);
+
+        ItemStack is1 = new ItemStack(Material.LEATHER_LEGGINGS);
+        LeatherArmorMeta lam1 = (LeatherArmorMeta) is1.getItemMeta();
+        lam1.setColor(c);
+        is1.setItemMeta(lam1);
+        VersionUtil.setUnbreakable(is1);
+
+        ItemStack is2 = new ItemStack(Material.LEATHER_CHESTPLATE);
+        LeatherArmorMeta lam2 = (LeatherArmorMeta) is2.getItemMeta();
+        lam2.setColor(c);
+        is2.setItemMeta(lam2);
+        VersionUtil.setUnbreakable(is2);
+
+        ItemStack is3 = new ItemStack(Material.LEATHER_HELMET);
+        LeatherArmorMeta lam3 = (LeatherArmorMeta) is3.getItemMeta();
+        lam3.setColor(c);
+        is3.setItemMeta(lam3);
+        VersionUtil.setUnbreakable(is3);
+
+        ItemStack[] armor = new ItemStack[] {is, is1, is2, is3};
+        p.getInventory().setArmorContents(armor);
+    }
+
+    public boolean isMember(MWPlayer player) {
+        return members.contains(player);
+    }
+
+    public void win() {
+        int money = game.getArena().getMoney().getWin();
+        for (MWPlayer player : members) {
+            MoneyUtil.giveMoney(player.getUUID(), money);
+        }
+        for (MWPlayer p : game.getPlayers().values()) {
+            Player player = p.getPlayer();
+            if (player != null && player.isOnline())
+                VersionUtil.sendTitle(player, MessageConfig.getNativeMessage("title_won").replace("%team%", getFullname()),
+                        MessageConfig.getNativeMessage("subtitle_won"));
+        }
+        won = true;
+        Bukkit.getPluginManager().callEvent(new GameEndEvent(game, this));
+    }
+
+    public void lose() {
+        int money = game.getArena().getMoney().getLoss();
+        for (MWPlayer player : members) {
+            MoneyUtil.giveMoney(player.getUUID(), money);
+        }
+    }
+
+    public boolean isWon() {
+        return won;
+    }
+
+    public void updateIntervals(int newInterval) {
+        if (newInterval < currentInterval && currentInterval != 0) {
+            getGame().broadcast(MessageConfig.getMessage("team_buffed").replace("%team%", getFullname()));
+        }
+        if (newInterval > currentInterval && currentInterval != 0) {
+            getGame().broadcast(MessageConfig.getMessage("team_nerved").replace("%team%", getFullname()));
+        }
+        for (MWPlayer mwPlayer : members) {
+            mwPlayer.setPeriod(newInterval);
+        }
+        currentInterval = newInterval;
+    }
+}

+ 112 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/Area.java

@@ -0,0 +1,112 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.geometry;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+import org.bukkit.Location;
+import org.bukkit.configuration.serialization.ConfigurationSerializable;
+
+@ToString
+@AllArgsConstructor
+@Getter
+public class Area implements ConfigurationSerializable {
+
+    @SerializedName("min_x") private int minX;
+    @SerializedName("min_y") private int minY;
+    @SerializedName("min_z") private int minZ;
+    @SerializedName("max_x") private int maxX;
+    @SerializedName("max_y") private int maxY;
+    @SerializedName("max_z") private int maxZ;
+    private transient boolean checked;
+
+    public static Area deserialize(Map<String, Object> serialized) {
+        int minX = (int) serialized.get("min_x");
+        int minY = (int) serialized.get("min_y");
+        int minZ = (int) serialized.get("min_z");
+
+        int maxX = (int) serialized.get("max_x");
+        int maxY = (int) serialized.get("max_y");
+        int maxZ = (int) serialized.get("max_z");
+        return new Area(minX, minY, minZ, maxX, maxY, maxZ, false);
+    }
+
+    public static Area defaultAreaAround(Location location) {
+        return new Area(location.getBlockX() - 20,
+                location.getBlockY() - 20,
+                location.getBlockZ() - 20,
+                location.getBlockX() + 20,
+                location.getBlockY() + 20,
+                location.getBlockZ() + 20, true);
+    }
+
+    public boolean isInArea(double x, double y, double z) {
+        checkValues();
+        return x >= minX && x <= maxX &&
+                y >= minY && y <= maxY &&
+                z >= minZ && z <= maxZ;
+    }
+
+    public boolean isInArea(Location loc) {
+        double x = loc.getX();
+        double y = loc.getY();
+        double z = loc.getZ();
+        return isInArea(x, y, z);
+    }
+
+    void checkValues() {
+        if (checked)
+            return;
+
+        if (minX >= maxX) {
+            int oldMin = minX;
+            this.minX = maxX;
+            this.maxX = oldMin;
+        }
+
+        if (minY >= maxY) {
+            int oldMin = minY;
+            this.minY = maxY;
+            this.maxY = oldMin;
+        }
+
+        if (minZ >= maxZ) {
+            int oldMin = minZ;
+            this.minZ = maxZ;
+            this.maxZ = oldMin;
+        }
+        checked = true;
+    }
+
+    @Override
+    public Map<String, Object> serialize() {
+        Map<String, Object> serialized = new HashMap<>();
+        serialized.put("min_x", minX);
+        serialized.put("min_y", minY);
+        serialized.put("min_z", minZ);
+        serialized.put("max_x", maxX);
+        serialized.put("max_y", maxY);
+        serialized.put("max_z", maxZ);
+        return serialized;
+    }
+}

+ 40 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/FlatArea.java

@@ -0,0 +1,40 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.geometry;
+
+
+import org.bukkit.Location;
+
+public class FlatArea extends Area {
+
+    public FlatArea(int minX, int minZ, int maxX, int maxZ) {
+        super(minX, 0, minZ, maxX, 0, maxZ, false);
+    }
+
+    public boolean isInArea(double x, double z) {
+        return super.isInArea(x, 0, z);
+    }
+
+    @Override
+    public boolean isInArea(Location loc) {
+        long x = Math.round(loc.getX());
+        long z = Math.round(loc.getZ());
+        return isInArea(x, z);
+    }
+}

+ 71 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/Line.java

@@ -0,0 +1,71 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.geometry;
+
+import de.butzlabben.missilewars.util.MathUtil;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.bukkit.util.Vector;
+
+/**
+ * A 3-dimensional line in the form:
+ * supporter + t*direction
+ */
+@AllArgsConstructor
+@ToString
+@EqualsAndHashCode
+public class Line {
+
+    private final Vector support, direction;
+
+    public static Line fromPoints(final Vector point1, final Vector point2) {
+        return new Line(point1.clone(), point2.clone().subtract(point1.clone()));
+    }
+
+    public boolean isIn(final Vector point) {
+        final double tX = this.getTEquationSolved(this.support.getX(), this.direction.getX(), point.getX());
+        final double tY = this.getTEquationSolved(this.support.getY(), this.direction.getY(), point.getY());
+        final double tZ = this.getTEquationSolved(this.support.getZ(), this.direction.getZ(), point.getZ());
+        return MathUtil.closeEnoughEquals(tX, tY) && MathUtil.closeEnoughEquals(tX, tZ) && MathUtil.closeEnoughEquals(tY, tZ);
+    }
+
+    public double distance(final Vector point) {
+        final Vector closestPoint = this.closestPointTo(point);
+        return point.distance(closestPoint);
+    }
+
+    public Vector closestPointTo(final Vector point) {
+        if (this.isIn(point)) return point.clone();
+        final Plane helperPlane = new Plane(point, this.direction);
+        return helperPlane.getBreakThroughPoint(this).get();
+    }
+
+    private double getTEquationSolved(final double supportValue, final double directionValue, final double pointValue) {
+        return (pointValue - supportValue) / directionValue;
+    }
+
+    public Vector getSupport() {
+        return this.support.clone();
+    }
+
+    public Vector getDirection() {
+        return this.direction.clone();
+    }
+}

+ 69 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/geometry/Plane.java

@@ -0,0 +1,69 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.geometry;
+
+import de.butzlabben.missilewars.util.MathUtil;
+import java.util.Optional;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+import org.bukkit.util.Vector;
+
+@Getter
+@AllArgsConstructor
+@ToString
+public class Plane {
+
+    private final Vector support, normal;
+
+    public boolean isIn(Vector point) {
+        return MathUtil.closeEnoughEquals(point.clone().subtract(support).dot(normal), 0);
+    }
+
+    public Vector closestPointTo(Vector point) {
+        if (isIn(point)) return point.clone();
+
+        Line supportLine = new Line(point, normal);
+        // we can safely get the value, as we know that this plane and the line are not parallel
+        return getBreakThroughPoint(supportLine).get();
+    }
+
+    public double distance(Vector point) {
+        Vector closestPoint = closestPointTo(point);
+        return point.distance(closestPoint);
+    }
+
+    public double distanceSquared(Vector point) {
+        Vector closestPoint = closestPointTo(point);
+        return point.distanceSquared(closestPoint);
+    }
+
+    public Optional<Vector> getBreakThroughPoint(Line line) {
+        if (MathUtil.closeEnoughEquals(line.getDirection().dot(normal), 0)) return Optional.empty();
+        double d = support.dot(normal);
+        double a = normal.getX();
+        double b = normal.getY();
+        double c = normal.getZ();
+        Vector x = line.getSupport();
+        Vector y = line.getDirection();
+        double t = (d - a * x.getX() - b * x.getY() - c * x.getZ()) / (a * y.getX() + b * y.getY() + c * y.getZ());
+        Vector result = line.getSupport().add(line.getDirection().multiply(t)).clone();
+        return Optional.of(result);
+    }
+}

+ 119 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/Missile.java

@@ -0,0 +1,119 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import de.butzlabben.missilewars.wrapper.missile.paste.PasteProvider;
+import java.io.File;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Location;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.SpawnEggMeta;
+import org.bukkit.material.SpawnEgg;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Butzlabben
+ * @since 06.01.2018
+ */
+@SuppressWarnings("deprecation")
+@RequiredArgsConstructor
+public class Missile {
+
+    private final String schematic;
+    private final String name;
+    private final EntityType egg;
+    private final int down;
+    private final int dist;
+    private final int occurrence;
+
+    public void paste(Player p, MissileFacing mf, Game game) {
+        if (mf == null)
+            return;
+        try {
+            Location loc = p.getLocation();
+            Vector pastePos = new Vector(loc.getX(), loc.getY(), loc.getZ());
+            pastePos = pastePos.add(new Vector(0, -down, 0));
+
+            int rotation = 0;
+            if (mf == MissileFacing.NORTH) {
+                pastePos = pastePos.add(new Vector(0, 0, -dist));
+            } else if (mf == MissileFacing.SOUTH) {
+                pastePos = pastePos.add(new Vector(0, 0, dist));
+                rotation = 180;
+            } else if (mf == MissileFacing.EAST) {
+                pastePos = pastePos.add(new Vector(dist, 0, 0));
+                rotation = 270;
+            } else if (mf == MissileFacing.WEST) {
+                pastePos = pastePos.add(new Vector(-dist, 0, 0));
+                rotation = 90;
+            }
+
+
+            PasteProvider.getPaster().pasteMissile(getSchematic(), pastePos, rotation, loc.getWorld(),
+                    game.getPlayer(p).getTeam());
+        } catch (Exception e) {
+            Logger.ERROR.log("Could not load " + name);
+            e.printStackTrace();
+        }
+    }
+
+    public File getSchematic() {
+        File pluginDir = MissileWars.getInstance().getDataFolder();
+        File file = new File(pluginDir, "missiles/" + schematic);
+        return file;
+    }
+
+    public int occurrence() {
+        return occurrence;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public EntityType getType() {
+        return egg;
+    }
+
+    public ItemStack getItem() {
+        ItemStack is = new ItemStack(VersionUtil.getMonsterEgg(egg));
+        if (VersionUtil.getVersion() > 10) {
+            SpawnEggMeta sm = (SpawnEggMeta) is.getItemMeta();
+            if (VersionUtil.getVersion() < 13)
+                sm.setSpawnedType(egg);
+            is.setItemMeta(sm);
+        } else {
+            SpawnEgg se = new SpawnEgg(egg);
+            se.setSpawnedType(egg);
+            is = se.toItemStack();
+            is.setAmount(1);
+        }
+        ItemMeta im = is.getItemMeta();
+        im.setDisplayName(name);
+        is.setItemMeta(im);
+        return is;
+    }
+}

+ 125 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/MissileFacing.java

@@ -0,0 +1,125 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.wrapper.abstracts.arena.MissileConfiguration;
+import de.butzlabben.missilewars.wrapper.player.Interval;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.bukkit.entity.Player;
+
+/**
+ * @author Butzlabben
+ * @since 06.01.2018
+ */
+public enum MissileFacing {
+
+    NORTH(new Interval(180, 270), new Interval(135, 315)),
+    EAST(new Interval(270, 360), new Interval(225, 360), new Interval(0, 45)),
+    SOUTH(new Interval(0, 90), new Interval(0, 135), new Interval(315, 360)),
+    WEST(new Interval(90, 180), new Interval(45, 225));
+
+    public final Interval primary;
+    public final Interval[] secondary;
+
+    MissileFacing(Interval primary, Interval... secondary) {
+        this.primary = primary;
+        this.secondary = secondary;
+    }
+
+    public static MissileFacing getFacing(double degree, MissileConfiguration configuration) {
+        List<MissileFacing> values = Arrays.stream(MissileFacing.values()).filter(f -> configuration.getEnabledFacings().contains(f)).collect(Collectors.toList());
+        MissileFacing facing = null;
+        for (MissileFacing fac : values) {
+            if (fac.primary.isIn(degree)) {
+                facing = fac;
+                break;
+            }
+        }
+        if (facing == null) {
+            for (MissileFacing fac : values) {
+                for (int i = 0; i < fac.secondary.length; i++) {
+                    if (fac.secondary[i].isIn(degree)) {
+                        facing = fac;
+                        break;
+                    }
+                }
+            }
+        }
+        if (facing == null) {
+            Logger.WARN.log("Could not find direction for degree: " + degree);
+            facing = NORTH;
+        }
+        return facing;
+    }
+
+    public static MissileFacing getFacingPlayer(Player playerSelf, MissileConfiguration configuration) {
+        float y = playerSelf.getLocation().getYaw();
+        if (y < 0) {
+            y += 360;
+        }
+        y %= 360;
+        y += 45;
+        if (y > 360)
+            y -= 360;
+        return getFacing(y, configuration);
+    }
+
+    public static String getFacing(int i) {
+        String dir;
+        if (i == 0) {
+            dir = "west";
+        } else if (i == 1) {
+            dir = "west northwest";
+        } else if (i == 2) {
+            dir = "northwest";
+        } else if (i == 3) {
+            dir = "north northwest";
+        } else if (i == 4) {
+            dir = "north";
+        } else if (i == 5) {
+            dir = "north northeast";
+        } else if (i == 6) {
+            dir = "northeast";
+        } else if (i == 7) {
+            dir = "east northeast";
+        } else if (i == 8) {
+            dir = "east";
+        } else if (i == 9) {
+            dir = "east southeast";
+        } else if (i == 10) {
+            dir = "southeast";
+        } else if (i == 11) {
+            dir = "south southeast";
+        } else if (i == 12) {
+            dir = "south";
+        } else if (i == 13) {
+            dir = "south southwest";
+        } else if (i == 14) {
+            dir = "southwest";
+        } else if (i == 15) {
+            dir = "west southwest";
+        } else {
+            dir = "west";
+        }
+        return dir;
+    }
+}

+ 60 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/PasteProvider.java

@@ -0,0 +1,60 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile.paste;
+
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+
+/**
+ * @author Butzlabben
+ * @since 23.09.2018
+ */
+public class PasteProvider {
+
+    private static final Paster paster;
+
+    static {
+        if (VersionUtil.getVersion() < 13) {
+            paster = new R1_12PasteProvider();
+            Logger.DEBUG.log("Chose 1.12 normal paster");
+        } else {
+            if (MissileWars.getInstance().foundFAWE()) {
+                if (VersionUtil.getVersion() < 16) {
+                    paster = new R1_13FawePasteProvider();
+                    Logger.DEBUG.log("Chose 1.13 FAWE paster");
+                } else {
+                    paster = new R1_16FawePasteProvider();
+                    Logger.DEBUG.log("Chose 1.16 FAWE paster");
+                }
+            } else {
+                paster = new R1_13WEPasteProvider();
+                Logger.DEBUG.log("Chose 1.13 WE paster");
+            }
+        }
+    }
+
+    private PasteProvider() {
+    }
+
+    public static Paster getPaster() {
+        return paster;
+    }
+}

+ 34 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/Paster.java

@@ -0,0 +1,34 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile.paste;
+
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.io.File;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Butzlabben
+ * @since 23.09.2018
+ */
+public interface Paster {
+
+    void pasteMissile(File schematic, Vector position, int rotation, org.bukkit.World world, Team team);
+
+    void pasteSchematic(File schematic, Vector position, org.bukkit.World world);
+}

+ 44 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_12PasteProvider.java

@@ -0,0 +1,44 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile.paste;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.missile.paste.r1_12.R1_12Paster;
+import de.butzlabben.missilewars.util.version.ColorConverter;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.io.File;
+import org.bukkit.World;
+import org.bukkit.util.Vector;
+
+public class R1_12PasteProvider implements Paster {
+
+    R1_12Paster paster = new R1_12Paster();
+
+    @Override
+    public void pasteMissile(File schematic, Vector position, int rotation, World world, Team team) {
+        paster.pasteMissile(schematic, position, rotation, world, ColorConverter.getColorIDforBlockFromColorCode(team.getColorCode()),
+                Config.getReplaceRadius(), Config.getStartReplace(), MissileWars.getInstance(), Config.getReplaceTicks());
+    }
+
+    @Override
+    public void pasteSchematic(File schematic, Vector position, World world) {
+        paster.pasteSchematic(schematic, position, world);
+    }
+}

+ 43 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_13FawePasteProvider.java

@@ -0,0 +1,43 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile.paste;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.util.version.ColorConverter;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.io.File;
+import org.bukkit.World;
+import org.bukkit.util.Vector;
+
+public class R1_13FawePasteProvider implements Paster {
+
+    de.butzlabben.missilewars.missile.paste.r1_13.fawe.R1_13Paster platformPaster = new de.butzlabben.missilewars.missile.paste.r1_13.fawe.R1_13Paster();
+
+    @Override
+    public void pasteMissile(File schematic, Vector position, int rotation, World world, Team team) {
+        platformPaster.pasteMissile(schematic, position, rotation, world, ColorConverter.getGlassFromColorCode(team.getColorCode()),
+                Config.getReplaceRadius(), Config.getStartReplace(), MissileWars.getInstance(), Config.getReplaceTicks());
+    }
+
+    @Override
+    public void pasteSchematic(File schematic, Vector position, World world) {
+        platformPaster.pasteSchematic(schematic, position, world);
+    }
+}

+ 44 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_13WEPasteProvider.java

@@ -0,0 +1,44 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile.paste;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.missile.paste.r1_13.we.R1_13Paster;
+import de.butzlabben.missilewars.util.version.ColorConverter;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.io.File;
+import org.bukkit.World;
+import org.bukkit.util.Vector;
+
+public class R1_13WEPasteProvider implements Paster {
+
+    R1_13Paster paster = new R1_13Paster();
+
+    @Override
+    public void pasteMissile(File schematic, Vector position, int rotation, World world, Team team) {
+        paster.pasteMissile(schematic, position, rotation, world, ColorConverter.getGlassFromColorCode(team.getColorCode()),
+                Config.getReplaceRadius(), Config.getStartReplace(), MissileWars.getInstance(), Config.getReplaceTicks());
+    }
+
+    @Override
+    public void pasteSchematic(File schematic, Vector position, World world) {
+        paster.pasteSchematic(schematic, position, world);
+    }
+}

+ 46 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/missile/paste/R1_16FawePasteProvider.java

@@ -0,0 +1,46 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.missile.paste;
+
+import de.butzlabben.missilewars.Config;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.util.version.ColorConverter;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.io.File;
+import org.bukkit.World;
+import org.bukkit.util.Vector;
+
+/**
+ * @author Daniel Nägele
+ */
+public class R1_16FawePasteProvider implements Paster {
+
+    de.butzlabben.missilewars.missile.paste.r1_16.fawe.R1_16Paster platformPaster = new de.butzlabben.missilewars.missile.paste.r1_16.fawe.R1_16Paster();
+
+    @Override
+    public void pasteMissile(File schematic, Vector position, int rotation, World world, Team team) {
+        platformPaster.pasteMissile(schematic, position, rotation, world, ColorConverter.getGlassFromColorCode(team.getColorCode()),
+                Config.getReplaceRadius(), Config.getStartReplace(), MissileWars.getInstance(), Config.getReplaceTicks());
+    }
+
+    @Override
+    public void pasteSchematic(File schematic, Vector position, World world) {
+        platformPaster.pasteSchematic(schematic, position, world);
+    }
+}

+ 64 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/player/Interval.java

@@ -0,0 +1,64 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.player;
+
+import java.util.Objects;
+
+/**
+ * @author Butzlabben
+ * @since 19.01.2018
+ */
+public class Interval {
+
+    private int min;
+    private int max;
+
+    public Interval(int min, int max) {
+        this.max = max;
+        this.min = min;
+        if (min > max)
+            throw new IllegalArgumentException("Min value must be higher than max value");
+    }
+
+    public boolean isIn(double d) {
+        return d >= min && d <= max;
+    }
+
+    public Interval add(double d) {
+        return new Interval(min += d, max += d);
+    }
+
+    public boolean isIn(int d) {
+        return d >= min && d <= max;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(min, max);
+    }
+
+    @Override
+    public boolean equals(Object paramObject) {
+        if (paramObject instanceof Interval) {
+            Interval i = (Interval) paramObject;
+            return i.max == max && i.min == min;
+        }
+        return false;
+    }
+}

+ 96 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/player/MWPlayer.java

@@ -0,0 +1,96 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.player;
+
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.Randomizer;
+import de.butzlabben.missilewars.wrapper.game.Team;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+@EqualsAndHashCode(of = {"uuid", "id"})
+@Getter
+public class MWPlayer implements Runnable {
+
+    private static final AtomicLong NEXT_ID = new AtomicLong(0);
+    final long id = NEXT_ID.getAndIncrement();
+    private final UUID uuid;
+    private final Game game;
+    int i = -1;
+    private Team t;
+    private Randomizer r;
+    private int period;
+
+    public MWPlayer(Player player, Game game) {
+        this.uuid = player.getUniqueId();
+        this.game = game;
+    }
+
+    public Team getTeam() {
+        return t;
+    }
+
+    public void setTeam(Team t) {
+        this.t = t;
+    }
+
+    public Player getPlayer() {
+        return Bukkit.getPlayer(uuid);
+    }
+
+    public UUID getUUID() {
+        return uuid;
+    }
+
+    public void setPeriod(int period) {
+        this.period = period;
+    }
+
+    @Override
+    public void run() {
+        Player p = Bukkit.getPlayer(uuid);
+        if (p == null || !p.isOnline())
+            return;
+        if (i == -1) {
+            i = period - 10;
+            if (i >= period || i < 0) i = 0;
+        }
+        i++;
+        if (i >= period) {
+            if (r == null)
+                r = new Randomizer(game);
+            p.getInventory().addItem(r.createItem());
+            i = 0;
+        }
+        p.setLevel(period - i);
+    }
+
+    @Override
+    public String toString() {
+        return "MWPlayer(uuid=" + uuid + ", id=" + id + ", teamName=" + getTeam().getName() + ")";
+    }
+}

+ 104 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/player/PlayerData.java

@@ -0,0 +1,104 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.player;
+
+import com.google.common.base.Preconditions;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.ToString;
+import org.bukkit.GameMode;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.configuration.serialization.ConfigurationSerializable;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+@ToString
+@AllArgsConstructor
+public class PlayerData implements ConfigurationSerializable {
+
+    private final long time;
+    private UUID uuid;
+    private ItemStack[] contents;
+    private float exp;
+    private double health;
+    private int expLevel, foodLevel;
+    private GameMode gameMode;
+
+    public PlayerData(Player player) {
+        contents = player.getInventory().getContents();
+        exp = player.getExp();
+        expLevel = player.getLevel();
+        foodLevel = player.getFoodLevel();
+        health = player.getHealth();
+        gameMode = player.getGameMode();
+        uuid = player.getUniqueId();
+        time = System.currentTimeMillis();
+    }
+
+    public static PlayerData loadFromFile(File file) {
+        Preconditions.checkArgument(file.isFile(), file.getAbsolutePath() + " is not a file");
+        YamlConfiguration yamlConfiguration = YamlConfiguration.loadConfiguration(file);
+        PlayerData data;
+        if (VersionUtil.getVersion() > 12) {
+            data = yamlConfiguration.getSerializable("data", PlayerData.class);
+        } else {
+            data = (PlayerData) yamlConfiguration.get("data");
+        }
+        return data;
+    }
+
+    public void apply(Player player) {
+        Preconditions.checkArgument(player.getUniqueId().equals(uuid));
+        player.getInventory().setContents(contents);
+        player.setExp(exp);
+        player.setLevel(expLevel);
+        player.setHealth(Math.min(health, player.getMaxHealth()));
+        player.setFoodLevel(foodLevel);
+        player.setGameMode(gameMode);
+    }
+
+    public void saveToFile(String file) {
+        YamlConfiguration yamlConfiguration = new YamlConfiguration();
+        yamlConfiguration.set("data", this);
+        try {
+            yamlConfiguration.save(file);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public Map<String, Object> serialize() {
+        Map<String, Object> serialized = new HashMap<>();
+        serialized.put("uuid", uuid.toString());
+        serialized.put("gamemode", gameMode.name());
+        serialized.put("health", health);
+        serialized.put("food-level", foodLevel);
+        serialized.put("exp", exp);
+        serialized.put("exp-level", expLevel);
+        serialized.put("contents", contents);
+        serialized.put("time", time);
+        return serialized;
+    }
+}

+ 31 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/signs/CheckRunnable.java

@@ -0,0 +1,31 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.signs;
+
+import de.butzlabben.missilewars.MissileWars;
+import java.util.List;
+
+public class CheckRunnable implements Runnable {
+
+    @Override
+    public void run() {
+        List<MWSign> signs = MissileWars.getInstance().getSignRepository().getSigns();
+        signs.forEach(MWSign::update);
+    }
+}

+ 111 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/signs/MWSign.java

@@ -0,0 +1,111 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.signs;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MessageConfig;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.util.version.VersionUtil;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.block.Sign;
+
+@Data
+@AllArgsConstructor
+@EqualsAndHashCode(of = "location")
+public class MWSign {
+
+    private Location location;
+    private String lobby;
+
+    public boolean isValid() {
+        boolean worldExists = location.getWorld() != null;
+        boolean lobbyValid = GameManager.getInstance().getGames().containsKey(lobby);
+        boolean blockIsSign = VersionUtil.isWallSignMaterial(location.getBlock().getType());
+        return worldExists && lobbyValid && blockIsSign;
+    }
+
+    public boolean isLocation(Location location) {
+        return this.location.equals(location);
+    }
+
+    public void update() {
+        if (!isValid()) {
+            Logger.WARN.log("The specified configuration options for the sign at " + location + " for the lobby " + lobby + " are not valid.");
+            return;
+        }
+        Game game = GameManager.getInstance().getGame(getLobby());
+        List<String> lines = new ArrayList<>();
+        for (int i = 0; i < 4; i++) {
+            lines.add(replace(MessageConfig.getNativeMessage("sign." + i), game));
+        }
+        if (game == null) {
+            Logger.WARN.log("Could not find specifed arena \"" + getLobby() + "\" for sign at: " + getLocation().toString());
+        }
+        // Run sync
+        Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> editSign(getLocation(), lines));
+    }
+
+    public void editSign(Location location, List<String> lines) {
+        Block block = location.getBlock();
+        if (!VersionUtil.isWallSignMaterial(block.getType())) {
+            Logger.WARN.log("Configured sign at: " + location + " is not a wall sign");
+            return;
+        }
+        Sign sign = (Sign) block.getState();
+        for (int i = 0; i < lines.size(); i++) {
+            sign.setLine(i, lines.get(i));
+        }
+        sign.update(true);
+    }
+
+    private String replace(String line, Game game) {
+        String state = MessageConfig.getNativeMessage("sign.state.error");
+        String name = "No game";
+        if (game != null) {
+            switch (game.getState()) {
+                case LOBBY:
+                    state = MessageConfig.getNativeMessage("sign.state.lobby");
+                    name = game.getLobby().getDisplayName();
+                    break;
+                case INGAME:
+                    state = MessageConfig.getNativeMessage("sign.state.ingame");
+                    name = game.getArena().getDisplayName();
+                    break;
+                case END:
+                    state = MessageConfig.getNativeMessage("sign.state.ended");
+                    name = game.getArena().getDisplayName();
+                    break;
+            }
+        }
+        String replaced = line.replace("%state%", state).replace("%arena%", name);
+        int maxPlayers = game == null ? 0 : game.getLobby().getMaxSize();
+        int players = game == null ? 0 : game.getPlayers().size();
+        replaced = replaced.replace("%max_players%", Integer.toString(maxPlayers)).replace("%players%", Integer.toString(players));
+        return replaced;
+    }
+}

+ 93 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/wrapper/signs/SignRepository.java

@@ -0,0 +1,93 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.wrapper.signs;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.util.serialization.LocationTypeAdapter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import org.bukkit.Location;
+
+@Getter
+public class SignRepository {
+
+    private final static String PATH = "data";
+    private final static String FILE_NAME = "signs.json";
+
+    private final List<MWSign> signs = new ArrayList<>();
+
+    public static SignRepository load() {
+        File dir = new File(MissileWars.getInstance().getDataFolder(), PATH);
+        if (!dir.exists()) {
+            dir.mkdirs();
+            return null;
+        }
+        File file = new File(dir, FILE_NAME);
+        if (!file.exists())
+            return null;
+        Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapter(Location.class, new LocationTypeAdapter(true)).create();
+        try (InputStream in = new FileInputStream(file);
+             JsonReader reader = new JsonReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
+
+            return gson.fromJson(reader, SignRepository.class);
+        } catch (IOException e) {
+            Logger.WARN.log("Could not load missilewars signs: Error: " + e.getMessage());
+        }
+        return null;
+    }
+
+    public Optional<MWSign> getSign(Location location) {
+        return MissileWars.getInstance().getSignRepository().getSigns()
+                .stream().filter(sign -> sign.isLocation(location)).findAny();
+    }
+
+    public void save() {
+        File dir = new File(MissileWars.getInstance().getDataFolder(), PATH);
+        Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapter(Location.class, new LocationTypeAdapter(true)).create();
+        try (OutputStream out = new FileOutputStream(new File(dir, FILE_NAME));
+             JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
+            writer.setIndent("  ");
+            gson.toJson(this, SignRepository.class, writer);
+        } catch (Exception e) {
+            Logger.WARN.log("Could not save missilewars signs: Error: " + e.getMessage());
+        }
+    }
+
+    public List<MWSign> getSigns(Game game) {
+        return signs.stream().filter(s -> s.getLobby().equals(game.getArena().getName())).collect(Collectors.toList());
+    }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов