Compare commits
336
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc7ce154bd | ||
|
|
d18ddc7b07 | ||
|
|
b06ba40d16 | ||
|
|
3eca590c6f | ||
|
|
f6ac150a8f | ||
|
|
92a655e107 | ||
|
|
77a740edb7 | ||
|
|
4a87bb3fad | ||
|
|
ca0e364d85 | ||
|
|
fb88856925 | ||
|
|
3882f64289 | ||
|
|
6bda5b6adb | ||
|
|
ae3bde626f | ||
|
|
4add446940 | ||
|
|
d03a21d460 | ||
|
|
a91f550153 | ||
|
|
b8ba85f8c5 | ||
|
|
65c48eae45 | ||
|
|
c6bf0b0163 | ||
|
|
8b616ba2a2 | ||
|
|
6590513d56 | ||
|
|
0800793e64 | ||
|
|
882bacf57b | ||
|
|
ddcbdbaec5 | ||
|
|
7bd1e13660 | ||
|
|
974c5c5200 | ||
|
|
66dace7f2a | ||
|
|
f4faf763e3 | ||
|
|
ee265a858c | ||
|
|
84c3e7df13 | ||
|
|
675d87f6e9 | ||
|
|
86ae261c60 | ||
|
|
095db7e796 | ||
|
|
490d8b9891 | ||
|
|
097222a565 | ||
|
|
6bfb14db2b | ||
|
|
e3de258124 | ||
|
|
d08fd927dc | ||
|
|
067a6f41dc | ||
|
|
5b7910b697 | ||
|
|
1180af64a1 | ||
|
|
73fa6649af | ||
|
|
9dbee7d63b | ||
|
|
2c8c4fbf87 | ||
|
|
252758797d | ||
|
|
46ee734bbc | ||
|
|
a1ab06a80d | ||
|
|
1b79e35bd2 | ||
|
|
c3b30e4329 | ||
|
|
cf3fd2f38f | ||
|
|
c119913071 | ||
|
|
7d0a28b015 | ||
|
|
62a42e561e | ||
|
|
9dad34719b | ||
|
|
323e316f0f | ||
|
|
0a16e1e049 | ||
|
|
c371893407 | ||
|
|
f2bf8f1858 | ||
|
|
a0a33f1f56 | ||
|
|
164a160dbb | ||
|
|
efdbc6fe9f | ||
|
|
7ac02b4ec4 | ||
|
|
93971650ce | ||
|
|
336e3f934c | ||
|
|
91e350ee62 | ||
|
|
8736e78adf | ||
|
|
329ec8e1da | ||
|
|
ebf392b024 | ||
|
|
33d00e97c6 | ||
|
|
1fdcc11211 | ||
|
|
e0ed7ecdf5 | ||
|
|
2ca97c88fc | ||
|
|
fc502e3da8 | ||
|
|
5e3db1e78c | ||
|
|
fc6fd9ebb5 | ||
|
|
84edbcc0e4 | ||
|
|
9ec883d6ad | ||
|
|
2b0c7c1a9e | ||
|
|
df39093c69 | ||
|
|
dfbf87dcd4 | ||
|
|
8953a19400 | ||
|
|
85065bcc73 | ||
|
|
2209b42766 | ||
|
|
bf11bb0b70 | ||
|
|
b98d33af40 | ||
|
|
0d6b21701f | ||
|
|
b3240cdb22 | ||
|
|
89c1c4335b | ||
|
|
71f2da8e99 | ||
|
|
a257b604ea | ||
|
|
ef153d5d8f | ||
|
|
bd883a4fa1 | ||
|
|
ac7e04829e | ||
|
|
5cda58408a | ||
|
|
9767896cde | ||
|
|
f0e0cfbb85 | ||
|
|
de112f7e13 | ||
|
|
215259c6b9 | ||
|
|
36520a87f9 | ||
|
|
914aaff10b | ||
|
|
e015bbb356 | ||
|
|
ec262710ec | ||
|
|
1ac19014c1 | ||
|
|
dd1518fce4 | ||
|
|
04cb233604 | ||
|
|
2ff95f8450 | ||
|
|
6ed48895ca | ||
|
|
bff8cf24cd | ||
|
|
212f84b6de | ||
|
|
f7430c8fc8 | ||
|
|
c81a2d2161 | ||
|
|
2087b4c379 | ||
|
|
4d9548aafc | ||
|
|
4c63800189 | ||
|
|
b8725bc0f2 | ||
|
|
c90d767f0f | ||
|
|
143f4ee8eb | ||
|
|
ba2befb467 | ||
|
|
7a2b9b9763 | ||
|
|
0b9dc5358d | ||
|
|
448e9472db | ||
|
|
933c4c0db0 | ||
|
|
f27474016a | ||
|
|
17e5b2e049 | ||
|
|
d3512cb2eb | ||
|
|
7b19bfd39e | ||
|
|
0ab67bb426 | ||
|
|
29a362b580 | ||
|
|
a7f298682b | ||
|
|
895a51e71a | ||
|
|
4a5c24235b | ||
|
|
62c0250049 | ||
|
|
b4ccc3c4c8 | ||
|
|
239094971c | ||
|
|
53dbeff829 | ||
|
|
b5da06fd49 | ||
|
|
b55035f1f0 | ||
|
|
469cd19b55 | ||
|
|
91a28b4500 | ||
|
|
e745ff4721 | ||
|
|
23af3ff784 | ||
|
|
bc5c9a2a13 | ||
|
|
c220479052 | ||
|
|
0565f5de9e | ||
|
|
ff81c91661 | ||
|
|
1182cb2ed6 | ||
|
|
045f31d4ca | ||
|
|
263fd85df7 | ||
|
|
d70b025502 | ||
|
|
78f87d97f0 | ||
|
|
db13a9f0a2 | ||
|
|
e14c87c2fb | ||
|
|
9433495a52 | ||
|
|
4955e94306 | ||
|
|
09abfefe33 | ||
|
|
bd3546abc8 | ||
|
|
8a7a0453ce | ||
|
|
64d0d817c0 | ||
|
|
713561bf07 | ||
|
|
4be3e528b1 | ||
|
|
53fca580f3 | ||
|
|
20fb4bf9fb | ||
|
|
c42d259909 | ||
|
|
5c82c8d6da | ||
|
|
5910847172 | ||
|
|
aad1fcafa6 | ||
|
|
9fca7430a8 | ||
|
|
7c254707c1 | ||
|
|
9ee5f6e419 | ||
|
|
e49c3b1987 | ||
|
|
5ca4c70a41 | ||
|
|
040cae6cd1 | ||
|
|
324defc4a8 | ||
|
|
dc0003b91e | ||
|
|
d4a3c798f8 | ||
|
|
c88c2ab6aa | ||
|
|
32a20cd4c5 | ||
|
|
d7cc141b94 | ||
|
|
16d7347fd0 | ||
|
|
fdf3b5c73f | ||
|
|
74f17e1b6d | ||
|
|
0d18b81399 | ||
|
|
1fa5fdfeb7 | ||
|
|
3bec5f4cbd | ||
|
|
d38871eac9 | ||
|
|
238df2feff | ||
|
|
e752d7f73b | ||
|
|
b0df982be3 | ||
|
|
dc1b5957f6 | ||
|
|
4068eae5bb | ||
|
|
b1e3e99cb8 | ||
|
|
b3787983d5 | ||
|
|
43ef28499b | ||
|
|
6e1ef4fd7c | ||
|
|
ec2d243b7b | ||
|
|
ef6f34c2b2 | ||
|
|
977f4ff4ec | ||
|
|
337727b0f0 | ||
|
|
44dae51e1c | ||
|
|
035864631d | ||
|
|
f3b884058e | ||
|
|
03d4f4e6d8 | ||
|
|
7422a89d98 | ||
|
|
3590a5d278 | ||
|
|
15ac47b314 | ||
|
|
af644a71ee | ||
|
|
0ce69f207f | ||
|
|
76297bb3af | ||
|
|
1aad8f07c4 | ||
|
|
f26f4ed56a | ||
|
|
831eacaf47 | ||
|
|
c71a2567bd | ||
|
|
72e88ce491 | ||
|
|
66d84f4677 | ||
|
|
427aed9a7e | ||
|
|
0d1e6070ce | ||
|
|
220fb9e229 | ||
|
|
9acac488f2 | ||
|
|
d71c0d768e | ||
|
|
9ef4c2e96b | ||
|
|
5d33d2aff7 | ||
|
|
3f1065fd3a | ||
|
|
aa868deeca | ||
|
|
b6c298cec3 | ||
|
|
8f5a96dc31 | ||
|
|
2824c1053b | ||
|
|
ccf383cdb5 | ||
|
|
fce9449b7e | ||
|
|
69e971f618 | ||
|
|
b1f188dece | ||
|
|
a4289d5ac9 | ||
|
|
1fef363c50 | ||
|
|
558e6f84f1 | ||
|
|
bdbb8b5824 | ||
|
|
8093a4a644 | ||
|
|
50147a06e2 | ||
|
|
a52476650e | ||
|
|
0e5e841527 | ||
|
|
ea5279dd82 | ||
|
|
32cbbe6c51 | ||
|
|
9544c953a2 | ||
|
|
34df173940 | ||
|
|
ca99e6cfef | ||
|
|
b0414ae6ab | ||
|
|
c28d34ab88 | ||
|
|
9bae26044a | ||
|
|
d1b5d81fa7 | ||
|
|
e37e410542 | ||
|
|
956d2717d8 | ||
|
|
ef7232e687 | ||
|
|
ff31215295 | ||
|
|
a4a254ebbe | ||
|
|
71d9faa9f4 | ||
|
|
859733e3dd | ||
|
|
d94bbb7417 | ||
|
|
153a968776 | ||
|
|
639d06b01d | ||
|
|
fdbb525870 | ||
|
|
fcc2abdc49 | ||
|
|
a3729734cb | ||
|
|
90b623ea07 | ||
|
|
9f49f44075 | ||
|
|
e9a8e83019 | ||
|
|
7c81286feb | ||
|
|
e7cf3caae8 | ||
|
|
8742f5f631 | ||
|
|
2c0e264ece | ||
|
|
4592d53d22 | ||
|
|
6d0913fa0c | ||
|
|
092d33beb3 | ||
|
|
71d5d8303d | ||
|
|
49eeb646ea | ||
|
|
ceca038b27 | ||
|
|
76ceb9ef79 | ||
|
|
219879974c | ||
|
|
bd630ebb7a | ||
|
|
c56f318f1c | ||
|
|
4d98d7aa75 | ||
|
|
619190d0ae | ||
|
|
06641c5d84 | ||
|
|
2a52177043 | ||
|
|
fc067a2ae0 | ||
|
|
116a9c11a2 | ||
|
|
3f29ceb08f | ||
|
|
a33ee357e8 | ||
|
|
e36256d5be | ||
|
|
0e3a54a1b9 | ||
|
|
2e67b41b44 | ||
|
|
e4ac8f7a63 | ||
|
|
e89e9d2181 | ||
|
|
8faf0efd60 | ||
|
|
2f1aeb71ee | ||
|
|
6475a7b825 | ||
|
|
193d8d778f | ||
|
|
38da5b1d34 | ||
|
|
04e3ddb09f | ||
|
|
47db27a86e | ||
|
|
f13534da3f | ||
|
|
e45698c88a | ||
|
|
9197840873 | ||
|
|
63d8335b3a | ||
|
|
184617e9c3 | ||
|
|
696c4bc260 | ||
|
|
9004609c1b | ||
|
|
ddedcea8ea | ||
|
|
318a30fe54 | ||
|
|
4808d22f6a | ||
|
|
6b2a323a9c | ||
|
|
694ca0efba | ||
|
|
0276763a8d | ||
|
|
8811328571 | ||
|
|
b3c43f1763 | ||
|
|
86677c942f | ||
|
|
31581fc643 | ||
|
|
a412f5c24c | ||
|
|
eae979ee65 | ||
|
|
28b9b84e07 | ||
|
|
a5cdb93f1b | ||
|
|
1572096020 | ||
|
|
0ff9512a7f | ||
|
|
f0ffb3ad21 | ||
|
|
3c82939120 | ||
|
|
c9935637b9 | ||
|
|
b35c5b9240 | ||
|
|
39814dae05 | ||
|
|
7e3b043c98 | ||
|
|
5324749a64 | ||
|
|
b9f88bc4b2 | ||
|
|
8470115a74 | ||
|
|
0f976d2316 | ||
|
|
c52207298e | ||
|
|
fd902f4ec6 | ||
|
|
0a7052b6f5 | ||
|
|
0ea2d4958b | ||
|
|
e8534b42ac | ||
|
|
c468696537 |
No files matched your search
@@ -118,3 +118,5 @@ run/
|
||||
!gradle-wrapper.jar
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
|
||||
local.gradle
|
||||
@@ -1,3 +1,11 @@
|
||||
# Chatcoloring
|
||||
Der Spieler benötigt das Recht "chatcolor" damit dessen Namen blau im Chat angezeigt wird.
|
||||
|
||||
# Wandering Trader
|
||||
|
||||
Ich würde vorschlagen, das Plugin WanderingTrades zu verwenden, da es uns ermöglicht, eigene Handelsangebote festzulegen und gleichzeitig die Köpfe von Spielern unseres Servers zu verkaufen.
|
||||
|
||||
Da WanderingTrades auch Mini-Blöcke unterstützt, sollten wir überlegen, ob wir das Plugin MiniBlocks nicht vollständig weglassen. Was das Plugin nicht kann ist Player Heads zu droppen, wenn ein Spieler einen anderen tötet, dass müssten wir dann noch selber schreiben.
|
||||
|
||||
Zu tun wäre dann noch:
|
||||
Die Config des Plugins um Sniffing Potion und Heart of the Seas hinzuzufügen und Preis für Player Heads hinzufügen.
|
||||
@@ -0,0 +1,17 @@
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
implementation project(':core')
|
||||
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
|
||||
testImplementation platform('org.junit:junit-bom:5.13.4')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.core.util.entity.DisplayVillager;
|
||||
import eu.mhsl.craftattack.spawn.core.util.listener.DismissInventoryOpenFromHolder;
|
||||
import eu.mhsl.craftattack.spawn.core.util.listener.PlayerInteractAtEntityEventListener;
|
||||
import eu.mhsl.craftattack.spawn.core.util.server.PluginMessage;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The arena server: tribune, start pads, arenas, fights and resets.
|
||||
* The main server sends a copy of the player's inventory to {@code /api/arena/inventory} before connecting the player.
|
||||
*/
|
||||
public class Arena extends Appliance {
|
||||
public enum PlayerState {
|
||||
TRIBUNE,
|
||||
PAD,
|
||||
FIGHT,
|
||||
BUILD
|
||||
}
|
||||
|
||||
private record InventoryRequest(UUID player, String items) {
|
||||
}
|
||||
|
||||
private final Map<UUID, PlayerState> states = new HashMap<>();
|
||||
private ArenaWorlds worlds;
|
||||
private ArenaSettings settings;
|
||||
private Teams teams;
|
||||
private ArenaResetter resetter;
|
||||
private final List<ArenaSlot> slots = new ArrayList<>();
|
||||
private @Nullable Lobby lobby;
|
||||
private @Nullable DisplayVillager.ConfigBound villager;
|
||||
private @Nullable BukkitTask tickTask;
|
||||
|
||||
public Arena() {
|
||||
super("arena");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.worlds = new ArenaWorlds();
|
||||
this.villager = new DisplayVillager.ConfigBound(this.localConfig(), villager -> {
|
||||
villager.customName(Component.text("Zurück zu CraftAttack", NamedTextColor.GOLD));
|
||||
villager.setProfession(Villager.Profession.CARTOGRAPHER);
|
||||
villager.setVillagerType(Villager.Type.PLAINS);
|
||||
});
|
||||
this.villager.getVillager().addScoreboardTag(Displays.MANAGED_TAG);
|
||||
this.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the settings and builds pads, arenas and displays. Separate from onEnable so setup changes
|
||||
* can be applied without restarting the appliance.
|
||||
*/
|
||||
private void start() {
|
||||
this.settings = ArenaSettings.read(this.localConfig(), this.worlds.play());
|
||||
this.teams = new Teams();
|
||||
this.resetter = new ArenaResetter(this.worlds, this.settings.resetBlocksPerTick());
|
||||
|
||||
this.settings.arenas().stream()
|
||||
.filter(ArenaSettings.Slot::isComplete)
|
||||
.forEach(slot -> this.slots.add(new ArenaSlot(slot, this.worlds.play())));
|
||||
this.settings.arenas().stream()
|
||||
.filter(slot -> !slot.isComplete())
|
||||
.forEach(slot -> Main.logger().warning("Arena '" + slot.name() + "' is incomplete (region, spawn1, spawn2) and stays disabled"));
|
||||
|
||||
if(this.settings.padsComplete()) this.lobby = new Lobby(this, this.settings.pads());
|
||||
else Main.logger().warning("Arena: pads are not set up, fights cannot be started");
|
||||
|
||||
this.syncRegions().forEach(region -> this.worlds.keepLoaded(region, Main.instance()));
|
||||
this.removeStrayVillagers();
|
||||
this.placeStarterPlatform();
|
||||
this.syncAll();
|
||||
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> this.state(player) != PlayerState.BUILD)
|
||||
.forEach(this::toTribune);
|
||||
this.tickTask = Bukkit.getScheduler().runTaskTimer(Main.instance(), this::tick, 1, 1);
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if(this.tickTask != null) this.tickTask.cancel();
|
||||
this.slots.forEach(slot -> {
|
||||
if(slot.fight != null) slot.fight.abort();
|
||||
slot.remove();
|
||||
});
|
||||
this.slots.clear();
|
||||
if(this.lobby != null) this.lobby.shutdown();
|
||||
this.lobby = null;
|
||||
this.resetter.cancel();
|
||||
this.teams.clear();
|
||||
this.worlds.releaseChunks(Main.instance());
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
if(this.slots.stream().anyMatch(slot -> slot.state == ArenaSlot.State.FIGHT))
|
||||
throw new ApplianceCommand.Error("Es läuft gerade ein Kampf");
|
||||
if(this.lobby != null && !this.lobby.isIdle())
|
||||
throw new ApplianceCommand.Error("Gerade steht jemand auf einem Startfeld");
|
||||
this.stop();
|
||||
this.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the whole tribune area and all arenas from the template.
|
||||
*/
|
||||
public void syncAll() {
|
||||
this.slots.forEach(slot -> slot.state = ArenaSlot.State.RESET);
|
||||
List<Region> regions = this.syncRegions();
|
||||
for(int i = 0; i < regions.size(); i++) {
|
||||
boolean last = i == regions.size() - 1;
|
||||
this.resetter.reset(regions.get(i), () -> {
|
||||
if(!last) return;
|
||||
this.slots.forEach(slot -> slot.state = ArenaSlot.State.FREE);
|
||||
Main.logger().info("Arena: all regions synced from the template");
|
||||
});
|
||||
}
|
||||
if(regions.isEmpty()) this.slots.forEach(slot -> slot.state = ArenaSlot.State.FREE);
|
||||
}
|
||||
|
||||
public boolean isBusy() {
|
||||
return this.slots.stream().anyMatch(slot -> slot.state != ArenaSlot.State.FREE)
|
||||
|| (this.lobby != null && !this.lobby.isIdle());
|
||||
}
|
||||
|
||||
private List<Region> syncRegions() {
|
||||
return Stream.concat(
|
||||
Stream.ofNullable(this.settings.syncRegion()),
|
||||
this.slots.stream().map(slot -> slot.settings.region())
|
||||
).toList();
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
this.resetter.tick();
|
||||
if(this.lobby != null) this.lobby.tick();
|
||||
if(Bukkit.getCurrentTick() % 10 != 0) return;
|
||||
this.slots.forEach(ArenaSlot::updateDisplay);
|
||||
|
||||
// nobody takes damage outside of fights, so falling into the void would never end
|
||||
World play = this.worlds.play();
|
||||
play.getPlayers().stream()
|
||||
.filter(player -> player.getY() < play.getMinHeight() - 16 && this.state(player) != PlayerState.FIGHT)
|
||||
.forEach(this::toTribune);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes old copies of the return villager, for example after its position was changed in the config by hand.
|
||||
*/
|
||||
private void removeStrayVillagers() {
|
||||
Region region = this.settings.syncRegion();
|
||||
if(region == null || this.villager == null) return;
|
||||
UUID current = this.villager.getUniqueId();
|
||||
for(int cx = region.minChunkX(); cx <= region.maxChunkX(); cx++) {
|
||||
for(int cz = region.minChunkZ(); cz <= region.maxChunkZ(); cz++) {
|
||||
for(Entity entity : this.worlds.play().getChunkAt(cx, cz).getEntities()) {
|
||||
if(!(entity instanceof Villager) || entity.getUniqueId().equals(current)) continue;
|
||||
if(!entity.getScoreboardTags().contains(Displays.MANAGED_TAG)) continue;
|
||||
entity.remove();
|
||||
Main.logger().info("Arena: removed a stray return villager at " + entity.getLocation().toVector());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fresh, empty world gets a small platform at the tribune spawn, so the first visitors do not fall into the void.
|
||||
*/
|
||||
private void placeStarterPlatform() {
|
||||
Location spawn = this.settings.tribuneSpawn();
|
||||
if(spawn == null) return;
|
||||
for(World world : new World[]{this.worlds.play(), this.worlds.template()}) {
|
||||
Location below = spawn.clone();
|
||||
below.setWorld(world);
|
||||
below.subtract(0, 1, 0);
|
||||
if(!below.getBlock().getType().isAir()) return;
|
||||
}
|
||||
for(World world : new World[]{this.worlds.play(), this.worlds.template()}) {
|
||||
for(int dx = -3; dx <= 3; dx++) {
|
||||
for(int dz = -3; dz <= 3; dz++) {
|
||||
world.getBlockAt(spawn.getBlockX() + dx, spawn.getBlockY() - 1, spawn.getBlockZ() + dz).setType(Material.SMOOTH_STONE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Main.logger().info("Arena: placed a starter platform at the tribune spawn");
|
||||
}
|
||||
|
||||
boolean hasFreeSlot() {
|
||||
return this.slots.stream().anyMatch(slot -> slot.state == ArenaSlot.State.FREE);
|
||||
}
|
||||
|
||||
void startFight(List<List<Player>> teams, FightMode mode, Map<UUID, UUID> kits) {
|
||||
ArenaSlot slot = this.slots.stream().filter(s -> s.state == ArenaSlot.State.FREE).findFirst().orElse(null);
|
||||
if(slot == null) {
|
||||
teams.forEach(team -> team.forEach(this::toTribune));
|
||||
return;
|
||||
}
|
||||
new Fight(this, slot, teams, mode, kits).start();
|
||||
}
|
||||
|
||||
void resetSlot(ArenaSlot slot) {
|
||||
if(!this.slots.contains(slot)) return;
|
||||
slot.state = ArenaSlot.State.RESET;
|
||||
this.resetter.reset(slot.settings.region(), () -> {
|
||||
if(!this.slots.contains(slot)) return;
|
||||
slot.state = ArenaSlot.State.FREE;
|
||||
Location display = slot.settings.displayLocation(this.worlds.play());
|
||||
Style.play(display, Sound.BLOCK_BEACON_ACTIVATE, 1.5f, 1.2f);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings the player back to the tribune with the own inventory, locked.
|
||||
*/
|
||||
public void toTribune(Player player) {
|
||||
if(this.lobby != null) this.lobby.leave(player);
|
||||
this.setState(player, PlayerState.TRIBUNE);
|
||||
this.teams.leave(player);
|
||||
player.closeInventory();
|
||||
Location spawn = this.settings.tribuneSpawn();
|
||||
player.teleport(spawn != null ? spawn : this.worlds.play().getSpawnLocation());
|
||||
restore(player, GameMode.ADVENTURE);
|
||||
InventoryStore.apply(player, player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Full health and food, no effects, no fire.
|
||||
*/
|
||||
static void restore(Player player, GameMode gameMode) {
|
||||
player.setGameMode(gameMode);
|
||||
player.getActivePotionEffects().stream().map(PotionEffect::getType).forEach(player::removePotionEffect);
|
||||
player.setHealth(Objects.requireNonNull(player.getAttribute(Attribute.MAX_HEALTH)).getValue());
|
||||
player.setAbsorptionAmount(0);
|
||||
player.setFoodLevel(20);
|
||||
player.setSaturation(5);
|
||||
player.setExhaustion(0);
|
||||
player.setFireTicks(0);
|
||||
player.setFreezeTicks(0);
|
||||
player.setFallDistance(0);
|
||||
player.setArrowsInBody(0, false);
|
||||
player.setVelocity(new Vector());
|
||||
}
|
||||
|
||||
public void toggleBuild(Player player) {
|
||||
PlayerState state = this.state(player);
|
||||
if(state == PlayerState.BUILD) {
|
||||
Location target = player.getLocation().clone();
|
||||
target.setWorld(this.worlds.play());
|
||||
this.toTribune(player);
|
||||
if(!this.isInsideArena(target)) player.teleport(target);
|
||||
player.sendMessage(Style.message("Bau-Modus beendet. Änderungen an der Vorlage übernimmst du mit /arenasetup sync."));
|
||||
return;
|
||||
}
|
||||
if(state != PlayerState.TRIBUNE) throw new ApplianceCommand.Error("Nur von der Tribüne aus möglich");
|
||||
|
||||
this.setState(player, PlayerState.BUILD);
|
||||
this.teams.leave(player);
|
||||
Location target = player.getLocation().clone();
|
||||
target.setWorld(this.worlds.template());
|
||||
player.getInventory().clear();
|
||||
player.teleport(target);
|
||||
player.setGameMode(GameMode.CREATIVE);
|
||||
player.sendMessage(Style.message("Du bist in der Vorlage. Mit /build kommst du zurück."));
|
||||
}
|
||||
|
||||
void moveVillager(Location location) {
|
||||
Objects.requireNonNull(this.villager).updateLocation(location);
|
||||
}
|
||||
|
||||
List<String> arenaNames() {
|
||||
return this.settings.arenas().stream().map(ArenaSettings.Slot::name).toList();
|
||||
}
|
||||
|
||||
public void sendToCraftAttack(Player player) {
|
||||
PluginMessage.connect(player, this.settings.returnServer());
|
||||
}
|
||||
|
||||
public void handleQuit(Player player) {
|
||||
Fight fight = this.fightOf(player);
|
||||
if(fight != null && fight.isAlive(player.getUniqueId())) fight.forfeit(player.getUniqueId());
|
||||
if(this.lobby != null) this.lobby.leave(player);
|
||||
this.teams.leave(player);
|
||||
this.states.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
public PlayerState state(Player player) {
|
||||
return this.states.getOrDefault(player.getUniqueId(), PlayerState.TRIBUNE);
|
||||
}
|
||||
|
||||
void setState(Player player, PlayerState state) {
|
||||
this.states.put(player.getUniqueId(), state);
|
||||
}
|
||||
|
||||
/**
|
||||
* The fight the player is part of, also during the short celebration after it ended.
|
||||
*/
|
||||
public @Nullable Fight fightOf(Player player) {
|
||||
return this.slots.stream()
|
||||
.map(slot -> slot.fight)
|
||||
.filter(fight -> fight != null && fight.involves(player.getUniqueId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether blocks may change at this position of the play world: only inside arenas.
|
||||
*/
|
||||
public boolean isInsideArena(Location location) {
|
||||
return this.slots.stream().anyMatch(slot -> slot.settings.region().contains(location.getX(), location.getY(), location.getZ()));
|
||||
}
|
||||
|
||||
public @Nullable Region arenaRegionOf(Player player) {
|
||||
Fight fight = this.fightOf(player);
|
||||
return fight == null ? null : fight.region();
|
||||
}
|
||||
|
||||
public @Nullable Lobby lobby() {
|
||||
return this.lobby;
|
||||
}
|
||||
|
||||
public ArenaWorlds worlds() {
|
||||
return this.worlds;
|
||||
}
|
||||
|
||||
ArenaSettings settings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
Teams teams() {
|
||||
return this.teams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
|
||||
Gson gson = new Gson();
|
||||
apiBuilder.rawPost("inventory", (request, response) -> {
|
||||
InventoryRequest body = gson.fromJson(request.body(), InventoryRequest.class);
|
||||
if(body == null || body.player() == null || body.items() == null) throw new IllegalArgumentException("player and items are required");
|
||||
InventoryStore.put(body.player(), Base64.getDecoder().decode(body.items()));
|
||||
return HttpServer.nothing;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<ApplianceCommand<?>> commands() {
|
||||
return List.of(new BuildCommand(), new ArenaSetupCommand(), new CraftAttackCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<Listener> listeners() {
|
||||
UUID villager = Objects.requireNonNull(this.villager).getUniqueId();
|
||||
return List.of(
|
||||
new PlayerConnectionListener(),
|
||||
new LockListener(),
|
||||
new FightListener(),
|
||||
new WorldProtectionListener(),
|
||||
new ModeMenuListener(),
|
||||
new NoProgressListener(),
|
||||
new PlayerInteractAtEntityEventListener(villager, event -> this.sendToCraftAttack(event.getPlayer())),
|
||||
new DismissInventoryOpenFromHolder(villager)
|
||||
);
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.ResetCursor;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* Copies regions from the template world over the play world. Only blocks that differ are written,
|
||||
* without physics. Block entities (chests, signs, banners) are always copied, so changed contents are reset too.
|
||||
* The work is spread over ticks with a fixed block budget.
|
||||
*/
|
||||
final class ArenaResetter {
|
||||
private record Job(Region region, ResetCursor cursor, Runnable onDone) {
|
||||
}
|
||||
|
||||
private final World play;
|
||||
private final World template;
|
||||
private final int blocksPerTick;
|
||||
private final Queue<Job> jobs = new ArrayDeque<>();
|
||||
private Job current;
|
||||
|
||||
ArenaResetter(ArenaWorlds worlds, int blocksPerTick) {
|
||||
this.play = worlds.play();
|
||||
this.template = worlds.template();
|
||||
this.blocksPerTick = blocksPerTick;
|
||||
}
|
||||
|
||||
void reset(Region region, Runnable onDone) {
|
||||
this.jobs.add(new Job(region, new ResetCursor(region), onDone));
|
||||
}
|
||||
|
||||
boolean isBusy() {
|
||||
return this.current != null || !this.jobs.isEmpty();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
this.jobs.clear();
|
||||
this.current = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every server tick.
|
||||
*/
|
||||
void tick() {
|
||||
int budget = this.blocksPerTick;
|
||||
while(budget > 0) {
|
||||
if(this.current == null) {
|
||||
this.current = this.jobs.poll();
|
||||
if(this.current == null) return;
|
||||
this.prepare(this.current.region);
|
||||
}
|
||||
|
||||
budget -= this.current.cursor.advance(budget, this::copyBlock);
|
||||
if(!this.current.cursor.isDone()) return;
|
||||
|
||||
this.finish(this.current.region);
|
||||
Job done = this.current;
|
||||
this.current = null;
|
||||
try {
|
||||
done.onDone.run();
|
||||
} catch(Exception e) {
|
||||
Main.logger().warning("Arena: error after reset: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepare(Region region) {
|
||||
this.removeEntities(region);
|
||||
// empty containers first, otherwise replacing them would drop their contents
|
||||
for(BlockState state : this.blockEntities(this.play, region, false)) {
|
||||
if(state instanceof Container container) container.getInventory().clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyBlock(int x, int y, int z) {
|
||||
BlockData source = this.template.getBlockAt(x, y, z).getBlockData();
|
||||
Block target = this.play.getBlockAt(x, y, z);
|
||||
if(!target.getBlockData().equals(source)) target.setBlockData(source, false);
|
||||
}
|
||||
|
||||
private void finish(Region region) {
|
||||
for(BlockState state : this.blockEntities(this.template, region, true)) {
|
||||
state.copy(new Location(this.play, state.getX(), state.getY(), state.getZ())).update(true, false);
|
||||
}
|
||||
this.removeEntities(region);
|
||||
}
|
||||
|
||||
private List<BlockState> blockEntities(World world, Region region, boolean snapshot) {
|
||||
List<BlockState> states = new ArrayList<>();
|
||||
for(int cx = region.minChunkX(); cx <= region.maxChunkX(); cx++) {
|
||||
for(int cz = region.minChunkZ(); cz <= region.maxChunkZ(); cz++) {
|
||||
Chunk chunk = world.getChunkAt(cx, cz);
|
||||
for(BlockState state : chunk.getTileEntities(snapshot)) {
|
||||
if(region.contains(state.getX(), state.getY(), state.getZ())) states.add(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
private void removeEntities(Region region) {
|
||||
BoundingBox box = new BoundingBox(
|
||||
region.minX(), region.minY(), region.minZ(),
|
||||
region.maxX() + 1, region.maxY() + 1, region.maxZ() + 1
|
||||
);
|
||||
for(Entity entity : this.play.getNearbyEntities(box)) {
|
||||
if(entity instanceof Player) continue;
|
||||
if(entity.getScoreboardTags().contains(Displays.MANAGED_TAG)) continue;
|
||||
entity.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import eu.mhsl.craftattack.spawn.core.config.ConfigUtil;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Positions and limits from the {@code arena} config section. All positions refer to the play world,
|
||||
* no matter which world was stored.
|
||||
*/
|
||||
record ArenaSettings(
|
||||
@Nullable Location tribuneSpawn,
|
||||
@Nullable Region syncRegion,
|
||||
@Nullable Region[] pads,
|
||||
List<Slot> arenas,
|
||||
int fightSeconds,
|
||||
int countdownSeconds,
|
||||
int resetBlocksPerTick,
|
||||
String returnServer
|
||||
) {
|
||||
static final String[] PAD_KEYS = {"red", "blue"};
|
||||
|
||||
record Slot(String name, @Nullable Region region, @Nullable Location[] spawns, @Nullable Location display) {
|
||||
boolean isComplete() {
|
||||
return this.region != null && this.spawns[0] != null && this.spawns[1] != null;
|
||||
}
|
||||
|
||||
Location displayLocation(World world) {
|
||||
if(this.display != null) return this.display;
|
||||
return new Location(world, this.region.centerX(), this.region.maxY() + 1.5, this.region.centerZ());
|
||||
}
|
||||
}
|
||||
|
||||
static ArenaSettings read(ConfigurationSection config, World world) {
|
||||
Region[] pads = new Region[PAD_KEYS.length];
|
||||
for(int i = 0; i < PAD_KEYS.length; i++) {
|
||||
pads[i] = readRegion(config.getConfigurationSection("pads." + PAD_KEYS[i]));
|
||||
}
|
||||
|
||||
List<Slot> arenas = new ArrayList<>();
|
||||
ConfigurationSection arenaSection = config.getConfigurationSection("arenas");
|
||||
if(arenaSection != null) {
|
||||
for(String name : arenaSection.getKeys(false)) {
|
||||
ConfigurationSection slot = arenaSection.getConfigurationSection(name);
|
||||
if(slot == null) continue;
|
||||
arenas.add(new Slot(
|
||||
name,
|
||||
readRegion(slot.getConfigurationSection("region")),
|
||||
new Location[]{
|
||||
readLocation(slot.getConfigurationSection("spawn1"), world),
|
||||
readLocation(slot.getConfigurationSection("spawn2"), world)
|
||||
},
|
||||
readLocation(slot.getConfigurationSection("display"), world)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new ArenaSettings(
|
||||
readLocation(config.getConfigurationSection("tribuneSpawn"), world),
|
||||
readRegion(config.getConfigurationSection("syncRegion")),
|
||||
pads,
|
||||
arenas,
|
||||
Math.max(1, config.getInt("fightMinutes", 10)) * 60,
|
||||
Math.max(1, config.getInt("countdownSeconds", 5)),
|
||||
Math.max(1000, config.getInt("resetBlocksPerTick", 20000)),
|
||||
config.getString("connect-server-name", "craftattack")
|
||||
);
|
||||
}
|
||||
|
||||
boolean padsComplete() {
|
||||
return this.pads[0] != null && this.pads[1] != null;
|
||||
}
|
||||
|
||||
private static @Nullable Region readRegion(@Nullable ConfigurationSection section) {
|
||||
if(section == null) return null;
|
||||
List<Integer> a = section.getIntegerList("pos1");
|
||||
List<Integer> b = section.getIntegerList("pos2");
|
||||
if(a.size() != 3 || b.size() != 3) return null;
|
||||
return Region.of(a.get(0), a.get(1), a.get(2), b.get(0), b.get(1), b.get(2));
|
||||
}
|
||||
|
||||
private static @Nullable Location readLocation(@Nullable ConfigurationSection section, World world) {
|
||||
if(section == null) return null;
|
||||
Location location = ConfigUtil.Position.paseLocation(section);
|
||||
location.setWorld(world);
|
||||
return location;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.core.config.ConfigUtil;
|
||||
import eu.mhsl.craftattack.spawn.core.config.Configuration;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Setting up tribune, pads and arenas at the own position. Works in the play world and in the template.
|
||||
* Changes take effect with {@code /arenasetup reload}.
|
||||
*/
|
||||
class ArenaSetupCommand extends ApplianceCommand.PlayerChecked<Arena> {
|
||||
private static final String USAGE = String.join("\n",
|
||||
"/arenasetup tribune",
|
||||
"/arenasetup villager",
|
||||
"/arenasetup pad <red|blue> <pos1|pos2>",
|
||||
"/arenasetup arena <name> <pos1|pos2|spawn1|spawn2|display|remove>",
|
||||
"/arenasetup syncregion <pos1|pos2>",
|
||||
"/arenasetup sync",
|
||||
"/arenasetup reload"
|
||||
);
|
||||
|
||||
ArenaSetupCommand() {
|
||||
super("arenasetup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String permission() {
|
||||
return "admin";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if(args.length == 0) throw new Error("\n" + USAGE);
|
||||
Location location = this.getPlayer().getLocation().clone();
|
||||
location.setWorld(this.getAppliance().worlds().play());
|
||||
ConfigurationSection config = this.getAppliance().localConfig();
|
||||
|
||||
switch(args[0]) {
|
||||
case "tribune" -> this.writeLocation(config, "tribuneSpawn", location);
|
||||
case "villager" -> this.getAppliance().moveVillager(location);
|
||||
case "pad" -> {
|
||||
if(args.length < 3 || !List.of(ArenaSettings.PAD_KEYS).contains(args[1])) throw new Error(USAGE);
|
||||
this.writeCorner(config, "pads." + args[1], args[2], location);
|
||||
}
|
||||
case "arena" -> {
|
||||
if(args.length < 3) throw new Error(USAGE);
|
||||
String path = "arenas." + args[1];
|
||||
switch(args[2]) {
|
||||
case "pos1", "pos2" -> this.writeCorner(config, path + ".region", args[2], location);
|
||||
case "spawn1", "spawn2", "display" -> this.writeLocation(config, path + "." + args[2], location);
|
||||
case "remove" -> config.set(path, null);
|
||||
default -> throw new Error(USAGE);
|
||||
}
|
||||
}
|
||||
case "syncregion" -> {
|
||||
if(args.length < 2) throw new Error(USAGE);
|
||||
this.writeCorner(config, "syncRegion", args[1], location);
|
||||
}
|
||||
case "sync" -> {
|
||||
if(this.getAppliance().isBusy()) throw new Error("Es läuft gerade ein Kampf, Countdown oder Reset");
|
||||
this.getAppliance().syncAll();
|
||||
sender.sendMessage(Style.message("Die Spielwelt wird aus der Vorlage übernommen."));
|
||||
return;
|
||||
}
|
||||
case "reload" -> {
|
||||
this.getAppliance().reload();
|
||||
sender.sendMessage(Style.message("Einstellungen übernommen. Die Arenen werden aus der Vorlage aufgebaut."));
|
||||
return;
|
||||
}
|
||||
default -> throw new Error("\n" + USAGE);
|
||||
}
|
||||
|
||||
Configuration.saveChanges();
|
||||
sender.sendMessage(Style.message("Gespeichert. Mit /arenasetup reload übernehmen."));
|
||||
}
|
||||
|
||||
private void writeCorner(ConfigurationSection config, String path, String corner, Location location) {
|
||||
if(!corner.equals("pos1") && !corner.equals("pos2")) throw new Error(USAGE);
|
||||
config.set(path + "." + corner, List.of(location.getBlockX(), location.getBlockY(), location.getBlockZ()));
|
||||
}
|
||||
|
||||
private void writeLocation(ConfigurationSection config, String path, Location location) {
|
||||
ConfigurationSection section = Optional.ofNullable(config.getConfigurationSection(path)).orElseGet(() -> config.createSection(path));
|
||||
ConfigUtil.Position.writeLocation(section, location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
List<String> options = switch(args.length) {
|
||||
case 1 -> List.of("tribune", "villager", "pad", "arena", "syncregion", "sync", "reload");
|
||||
case 2 -> switch(args[0]) {
|
||||
case "pad" -> Arrays.asList(ArenaSettings.PAD_KEYS);
|
||||
case "syncregion" -> List.of("pos1", "pos2");
|
||||
case "arena" -> this.getAppliance().arenaNames();
|
||||
default -> List.of();
|
||||
};
|
||||
case 3 -> switch(args[0]) {
|
||||
case "pad" -> List.of("pos1", "pos2");
|
||||
case "arena" -> List.of("pos1", "pos2", "spawn1", "spawn2", "display", "remove");
|
||||
default -> List.of();
|
||||
};
|
||||
default -> List.of();
|
||||
};
|
||||
return this.tabCompleteReducer(options, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.TextDisplay;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* One arena: free, in a fight or being reset.
|
||||
*/
|
||||
final class ArenaSlot {
|
||||
enum State {
|
||||
FREE,
|
||||
FIGHT,
|
||||
RESET
|
||||
}
|
||||
|
||||
final ArenaSettings.Slot settings;
|
||||
State state = State.RESET;
|
||||
@Nullable Fight fight;
|
||||
private final TextDisplay display;
|
||||
|
||||
ArenaSlot(ArenaSettings.Slot settings, World world) {
|
||||
this.settings = settings;
|
||||
// seen from the tribune across half an arena, so twice the size
|
||||
this.display = Displays.spawn(settings.displayLocation(world), 2f);
|
||||
}
|
||||
|
||||
void updateDisplay() {
|
||||
Component title = Component.text("Arena " + this.settings.name(), NamedTextColor.GOLD);
|
||||
Component status = switch(this.state) {
|
||||
case FREE -> Component.text("Frei", NamedTextColor.GREEN);
|
||||
case RESET -> Component.text("Wird zurückgesetzt", NamedTextColor.YELLOW);
|
||||
case FIGHT -> this.fight == null
|
||||
? Component.text("Kampf", NamedTextColor.RED)
|
||||
: this.fight.headline().appendNewline().append(Component.text(Style.time(this.fight.secondsLeft()), NamedTextColor.WHITE));
|
||||
};
|
||||
this.display.text(title.appendNewline().append(status));
|
||||
}
|
||||
|
||||
void remove() {
|
||||
this.display.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameRules;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* The play world and its template. Players only fight in the play world; admins build in the template,
|
||||
* which is copied over the play world on every reset.
|
||||
*/
|
||||
final class ArenaWorlds {
|
||||
static final String PLAY = "arena";
|
||||
static final String TEMPLATE = "arena_template";
|
||||
|
||||
private static final class VoidGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private final World play;
|
||||
private final World template;
|
||||
|
||||
ArenaWorlds() {
|
||||
this.play = load(PLAY);
|
||||
this.template = load(TEMPLATE);
|
||||
|
||||
for(World world : new World[]{this.play, this.template}) {
|
||||
world.setGameRule(GameRules.ADVANCE_TIME, false);
|
||||
world.setGameRule(GameRules.ADVANCE_WEATHER, false);
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, false);
|
||||
world.setGameRule(GameRules.SPAWN_MONSTERS, false);
|
||||
world.setGameRule(GameRules.SPAWN_PHANTOMS, false);
|
||||
world.setGameRule(GameRules.SPAWN_PATROLS, false);
|
||||
world.setGameRule(GameRules.SPAWN_WANDERING_TRADERS, false);
|
||||
world.setGameRule(GameRules.SPAWN_WARDENS, false);
|
||||
world.setGameRule(GameRules.RAIDS, false);
|
||||
world.setGameRule(GameRules.SHOW_DEATH_MESSAGES, false);
|
||||
world.setGameRule(GameRules.SHOW_ADVANCEMENT_MESSAGES, false);
|
||||
world.setStorm(false);
|
||||
world.setThundering(false);
|
||||
world.setTime(6000);
|
||||
}
|
||||
this.play.setGameRule(GameRules.IMMEDIATE_RESPAWN, true);
|
||||
this.play.setGameRule(GameRules.KEEP_INVENTORY, true);
|
||||
this.template.setGameRule(GameRules.RANDOM_TICK_SPEED, 0);
|
||||
this.template.setGameRule(GameRules.FIRE_SPREAD_RADIUS_AROUND_PLAYER, 0);
|
||||
}
|
||||
|
||||
private static World load(String name) {
|
||||
World world = Bukkit.getWorld(name);
|
||||
if(world != null) return world;
|
||||
return Objects.requireNonNull(
|
||||
new WorldCreator(name).generator(new VoidGenerator()).generateStructures(false).createWorld(),
|
||||
"Could not create world " + name
|
||||
);
|
||||
}
|
||||
|
||||
World play() {
|
||||
return this.play;
|
||||
}
|
||||
|
||||
World template() {
|
||||
return this.template;
|
||||
}
|
||||
|
||||
boolean isPlay(World world) {
|
||||
return this.play.equals(world);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps all chunks of the region loaded in both worlds, so resets never wait for chunk loading.
|
||||
*/
|
||||
void keepLoaded(Region region, Plugin plugin) {
|
||||
for(int cx = region.minChunkX(); cx <= region.maxChunkX(); cx++) {
|
||||
for(int cz = region.minChunkZ(); cz <= region.maxChunkZ(); cz++) {
|
||||
this.play.addPluginChunkTicket(cx, cz, plugin);
|
||||
this.template.addPluginChunkTicket(cx, cz, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void releaseChunks(Plugin plugin) {
|
||||
this.play.removePluginChunkTickets(plugin);
|
||||
this.template.removePluginChunkTickets(plugin);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Switches between the play world and the template at the same position.
|
||||
*/
|
||||
class BuildCommand extends ApplianceCommand.PlayerChecked<Arena> {
|
||||
BuildCommand() {
|
||||
super("build");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String permission() {
|
||||
return "arena.build";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
this.getAppliance().toggleBuild(this.getPlayer());
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class CraftAttackCommand extends ApplianceCommand.PlayerChecked<Arena> {
|
||||
CraftAttackCommand() {
|
||||
super("craftattack");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
this.getAppliance().sendToCraftAttack(this.getPlayer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Display;
|
||||
import org.bukkit.entity.TextDisplay;
|
||||
import org.bukkit.util.Transformation;
|
||||
|
||||
/**
|
||||
* Floating texts above pads and arenas. They are not saved with the world and come back with every start.
|
||||
*/
|
||||
final class Displays {
|
||||
static final String MANAGED_TAG = "arena_managed";
|
||||
|
||||
private Displays() {
|
||||
}
|
||||
|
||||
static TextDisplay spawn(Location location) {
|
||||
return spawn(location, 1f);
|
||||
}
|
||||
|
||||
static TextDisplay spawn(Location location, float scale) {
|
||||
// displays are not saved, they would vanish with an unloaded chunk
|
||||
location.getWorld().addPluginChunkTicket(location.getBlockX() >> 4, location.getBlockZ() >> 4, Main.instance());
|
||||
return location.getWorld().spawn(location, TextDisplay.class, display -> {
|
||||
display.setPersistent(false);
|
||||
display.setBillboard(Display.Billboard.CENTER);
|
||||
display.setAlignment(TextDisplay.TextAlignment.CENTER);
|
||||
display.setShadowed(true);
|
||||
display.setBackgroundColor(Color.fromARGB(110, 0, 0, 0));
|
||||
display.setLineWidth(220);
|
||||
display.addScoreboardTag(MANAGED_TAG);
|
||||
if(scale != 1f) {
|
||||
Transformation transformation = display.getTransformation();
|
||||
transformation.getScale().set(scale);
|
||||
display.setTransformation(transformation);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A running fight in one arena between team 0 (red pad, spawn 1) and team 1 (blue pad, spawn 2).
|
||||
* A team loses once none of its players is left in the arena.
|
||||
*/
|
||||
final class Fight {
|
||||
private static final int CELEBRATION_TICKS = 60;
|
||||
private static final double SPECTATOR_RANGE = 32;
|
||||
private static final double SPAWN_SPREAD = 2;
|
||||
|
||||
private final Arena arena;
|
||||
private final ArenaSlot slot;
|
||||
private final List<List<UUID>> teams;
|
||||
private final Map<UUID, String> names = new HashMap<>();
|
||||
private final List<Set<UUID>> alive;
|
||||
private final FightMode mode;
|
||||
private final Map<UUID, UUID> kits;
|
||||
private final BossBar bossBar;
|
||||
private final Set<UUID> bossBarViewers = new HashSet<>();
|
||||
private int secondsLeft;
|
||||
private boolean over;
|
||||
private @Nullable BukkitTask timer;
|
||||
|
||||
Fight(Arena arena, ArenaSlot slot, List<List<Player>> teams, FightMode mode, Map<UUID, UUID> kits) {
|
||||
this.arena = arena;
|
||||
this.slot = slot;
|
||||
this.teams = teams.stream().map(team -> team.stream().map(Player::getUniqueId).toList()).toList();
|
||||
teams.forEach(team -> team.forEach(player -> this.names.put(player.getUniqueId(), player.getName())));
|
||||
this.alive = this.teams.stream().map(team -> (Set<UUID>) new LinkedHashSet<>(team)).toList();
|
||||
this.mode = mode;
|
||||
this.kits = kits;
|
||||
this.secondsLeft = arena.settings().fightSeconds();
|
||||
this.bossBar = BossBar.bossBar(Component.empty(), 1f, BossBar.Color.RED, BossBar.Overlay.NOTCHED_10);
|
||||
}
|
||||
|
||||
void start() {
|
||||
this.slot.state = ArenaSlot.State.FIGHT;
|
||||
this.slot.fight = this;
|
||||
|
||||
for(int team = 0; team < 2; team++) {
|
||||
List<UUID> members = this.teams.get(team);
|
||||
for(int i = 0; i < members.size(); i++) {
|
||||
Player player = Bukkit.getPlayer(members.get(i));
|
||||
if(player == null) continue;
|
||||
this.arena.setState(player, Arena.PlayerState.FIGHT);
|
||||
this.arena.teams().join(player, Teams.fight(team));
|
||||
player.closeInventory();
|
||||
player.teleport(this.spawnFor(team, i, members.size()));
|
||||
Arena.restore(player, GameMode.SURVIVAL);
|
||||
InventoryStore.apply(player, this.kits.getOrDefault(player.getUniqueId(), player.getUniqueId()));
|
||||
|
||||
player.showTitle(Title.title(
|
||||
Component.text("Kampf!", NamedTextColor.GOLD),
|
||||
Component.text("gegen ", NamedTextColor.GRAY).append(this.teamNames(1 - team)),
|
||||
Title.Times.times(Duration.ZERO, Duration.ofMillis(1500), Duration.ofMillis(500))
|
||||
));
|
||||
Style.play(player, Sound.EVENT_RAID_HORN, 1.2f);
|
||||
}
|
||||
}
|
||||
// players who left during the countdown never arrived
|
||||
for(int team = 0; team < 2; team++) this.alive.get(team).removeIf(uuid -> Bukkit.getPlayer(uuid) == null);
|
||||
|
||||
Bukkit.broadcast(Style.message(
|
||||
this.teamNames(0).append(Component.text(" gegen ", NamedTextColor.GRAY)).append(this.teamNames(1))
|
||||
.append(Component.text(" in Arena " + this.slot.settings.name() + ". Modus: " + this.mode.displayName(this.isTeamFight()), NamedTextColor.GRAY))
|
||||
));
|
||||
this.updateBossBar();
|
||||
this.timer = Bukkit.getScheduler().runTaskTimer(Main.instance(), this::tickSecond, 20, 20);
|
||||
if(this.alive.get(0).isEmpty() || this.alive.get(1).isEmpty()) this.checkEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Team members stand in a small circle around the spawn, facing the same way.
|
||||
*/
|
||||
private Location spawnFor(int team, int index, int count) {
|
||||
Location center = this.slot.settings.spawns()[team];
|
||||
if(count == 1) return center;
|
||||
double angle = 2 * Math.PI * index / count;
|
||||
Location spot = center.clone().add(Math.cos(angle) * SPAWN_SPREAD, 0, Math.sin(angle) * SPAWN_SPREAD);
|
||||
boolean free = spot.getBlock().isPassable() && spot.clone().add(0, 1, 0).getBlock().isPassable()
|
||||
&& !spot.clone().subtract(0, 1, 0).getBlock().isPassable();
|
||||
return free ? spot : center;
|
||||
}
|
||||
|
||||
private boolean isTeamFight() {
|
||||
return this.teams.get(0).size() > 1 || this.teams.get(1).size() > 1;
|
||||
}
|
||||
|
||||
private Component teamNames(int team) {
|
||||
return Component.join(
|
||||
JoinConfiguration.separator(Component.text(", ", NamedTextColor.GRAY)),
|
||||
this.teams.get(team).stream().map(uuid -> Component.text(this.names.get(uuid), Style.PAD_TEXT_COLORS[team])).toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Short form for bossbar and displays: names in a duel, "Rot 2/3" in a team fight.
|
||||
*/
|
||||
Component headline() {
|
||||
if(!this.isTeamFight()) {
|
||||
return this.teamNames(0).append(Component.text(" gegen ", NamedTextColor.GRAY)).append(this.teamNames(1));
|
||||
}
|
||||
return Component.text("Rot " + this.alive.get(0).size() + "/" + this.teams.get(0).size(), Style.PAD_TEXT_COLORS[0])
|
||||
.append(Component.text(" gegen ", NamedTextColor.GRAY))
|
||||
.append(Component.text("Blau " + this.alive.get(1).size() + "/" + this.teams.get(1).size(), Style.PAD_TEXT_COLORS[1]));
|
||||
}
|
||||
|
||||
int secondsLeft() {
|
||||
return this.secondsLeft;
|
||||
}
|
||||
|
||||
boolean isOver() {
|
||||
return this.over;
|
||||
}
|
||||
|
||||
boolean involves(UUID player) {
|
||||
return this.teams.get(0).contains(player) || this.teams.get(1).contains(player);
|
||||
}
|
||||
|
||||
boolean isAlive(UUID player) {
|
||||
return this.alive.get(0).contains(player) || this.alive.get(1).contains(player);
|
||||
}
|
||||
|
||||
boolean sameTeam(UUID a, UUID b) {
|
||||
int team = this.teamOf(a);
|
||||
return team != -1 && team == this.teamOf(b);
|
||||
}
|
||||
|
||||
Region region() {
|
||||
return this.slot.settings.region();
|
||||
}
|
||||
|
||||
ArenaSlot slot() {
|
||||
return this.slot;
|
||||
}
|
||||
|
||||
private void tickSecond() {
|
||||
if(this.over) return;
|
||||
if(--this.secondsLeft <= 0) {
|
||||
this.timeUp();
|
||||
return;
|
||||
}
|
||||
if(this.secondsLeft == 60 || this.secondsLeft == 10) {
|
||||
this.alivePlayers().forEach(player -> {
|
||||
player.sendActionBar(Component.text("Noch " + this.secondsLeft + " Sekunden", NamedTextColor.YELLOW));
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_HAT, 0.8f);
|
||||
});
|
||||
}
|
||||
this.updateBossBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* More survivors win, then more hearts left, otherwise a draw.
|
||||
*/
|
||||
private void timeUp() {
|
||||
int red = this.alive.get(0).size();
|
||||
int blue = this.alive.get(1).size();
|
||||
if(red != blue) {
|
||||
this.finish(red > blue ? 0 : 1, Component.text("(Zeit abgelaufen, mehr Überlebende)", NamedTextColor.GRAY));
|
||||
return;
|
||||
}
|
||||
double redHealth = this.health(0);
|
||||
double blueHealth = this.health(1);
|
||||
if(Math.abs(redHealth - blueHealth) >= 0.5) {
|
||||
this.finish(redHealth > blueHealth ? 0 : 1, Component.text("(Zeit abgelaufen, mehr Herzen übrig)", NamedTextColor.GRAY));
|
||||
return;
|
||||
}
|
||||
this.finish(-1, Component.text("Die Zeit ist abgelaufen.", NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
private double health(int team) {
|
||||
return this.alive.get(team).stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(player -> player != null)
|
||||
.mapToDouble(player -> player.getHealth() + player.getAbsorptionAmount())
|
||||
.sum();
|
||||
}
|
||||
|
||||
private void updateBossBar() {
|
||||
this.bossBar.name(this.headline().append(Component.text(" " + Style.time(this.secondsLeft), NamedTextColor.WHITE)));
|
||||
this.bossBar.progress(Math.clamp((float) this.secondsLeft / this.arena.settings().fightSeconds(), 0f, 1f));
|
||||
|
||||
Set<UUID> viewers = new HashSet<>();
|
||||
for(Player player : this.arena.worlds().play().getPlayers()) {
|
||||
if(this.isAlive(player.getUniqueId()) || this.isSpectating(player)) viewers.add(player.getUniqueId());
|
||||
}
|
||||
for(UUID uuid : this.bossBarViewers) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player != null && !viewers.contains(uuid)) player.hideBossBar(this.bossBar);
|
||||
}
|
||||
for(UUID uuid : viewers) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player != null) player.showBossBar(this.bossBar);
|
||||
}
|
||||
this.bossBarViewers.clear();
|
||||
this.bossBarViewers.addAll(viewers);
|
||||
}
|
||||
|
||||
private boolean isSpectating(Player player) {
|
||||
if(this.arena.state(player) != Arena.PlayerState.TRIBUNE && this.arena.state(player) != Arena.PlayerState.PAD) return false;
|
||||
Region region = this.slot.settings.region();
|
||||
double dx = Math.max(0, Math.max(region.minX() - player.getX(), player.getX() - (region.maxX() + 1)));
|
||||
double dz = Math.max(0, Math.max(region.minZ() - player.getZ(), player.getZ() - (region.maxZ() + 1)));
|
||||
return Math.hypot(dx, dz) <= SPECTATOR_RANGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The player took lethal damage: out of the fight and back to the tribune.
|
||||
*/
|
||||
void defeated(Player player) {
|
||||
this.knockOut(player, Component.empty(), "ist ausgeschieden");
|
||||
}
|
||||
|
||||
/**
|
||||
* The player got out of the box, for example by glitching through a wall with an ender pearl.
|
||||
*/
|
||||
void leftBox(Player player) {
|
||||
this.knockOut(player, Component.text("Du hast die Arena verlassen", NamedTextColor.GRAY), "hat die Arena verlassen");
|
||||
}
|
||||
|
||||
private void knockOut(Player player, Component subtitle, String what) {
|
||||
if(this.over || !this.eliminate(player.getUniqueId())) return;
|
||||
player.showTitle(Title.title(Component.text("Ausgeschieden", NamedTextColor.RED), subtitle));
|
||||
Style.play(player, Sound.BLOCK_BEACON_DEACTIVATE, 1f);
|
||||
this.arena.toTribune(player);
|
||||
this.announceElimination(player.getUniqueId(), what);
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* The player left the server during the fight.
|
||||
*/
|
||||
void forfeit(UUID quitter) {
|
||||
if(this.over || !this.eliminate(quitter)) return;
|
||||
this.announceElimination(quitter, "hat die Arena verlassen");
|
||||
this.checkEnd();
|
||||
}
|
||||
|
||||
private boolean eliminate(UUID player) {
|
||||
int team = this.teamOf(player);
|
||||
return team != -1 && this.alive.get(team).remove(player);
|
||||
}
|
||||
|
||||
private void announceElimination(UUID player, String what) {
|
||||
if(!this.isTeamFight()) return;
|
||||
int team = this.teamOf(player);
|
||||
Component message = Component.text(this.names.get(player), Style.PAD_TEXT_COLORS[team])
|
||||
.append(Component.text(" " + what + ". ", NamedTextColor.GRAY))
|
||||
.append(this.headline());
|
||||
this.alivePlayers().forEach(fighter -> fighter.sendActionBar(message));
|
||||
this.updateBossBar();
|
||||
}
|
||||
|
||||
private void checkEnd() {
|
||||
if(this.over) return;
|
||||
boolean redOut = this.alive.get(0).isEmpty();
|
||||
boolean blueOut = this.alive.get(1).isEmpty();
|
||||
if(redOut && blueOut) this.finish(-1, Component.text("Niemand ist mehr übrig.", NamedTextColor.GRAY));
|
||||
else if(redOut) this.finish(1, this.detail(1));
|
||||
else if(blueOut) this.finish(0, this.detail(0));
|
||||
}
|
||||
|
||||
private Component detail(int winner) {
|
||||
String hearts = Style.hearts(this.health(winner));
|
||||
String survivors = this.isTeamFight() ? this.alive.get(winner).size() + " übrig, " : "";
|
||||
return Component.text("(" + this.mode.displayName(this.isTeamFight()) + ", " + survivors + hearts + " ❤)", NamedTextColor.GRAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the fight without a winner and without celebration, for example on shutdown.
|
||||
*/
|
||||
void abort() {
|
||||
if(this.over) return;
|
||||
this.over = true;
|
||||
this.stopTimer();
|
||||
this.hideBossBar();
|
||||
this.alivePlayers().forEach(this.arena::toTribune);
|
||||
this.slot.fight = null;
|
||||
}
|
||||
|
||||
private void finish(int winner, Component detail) {
|
||||
this.over = true;
|
||||
this.stopTimer();
|
||||
|
||||
if(winner == -1) {
|
||||
Bukkit.broadcast(Style.message(
|
||||
this.teamNames(0).append(Component.text(" und ", NamedTextColor.GRAY)).append(this.teamNames(1))
|
||||
.append(Component.text(" trennen sich unentschieden. ", NamedTextColor.GRAY)).append(detail)
|
||||
));
|
||||
this.alivePlayers().forEach(player -> {
|
||||
player.showTitle(Title.title(Component.text("Unentschieden", NamedTextColor.YELLOW), detail));
|
||||
Style.play(player, Sound.BLOCK_BEACON_DEACTIVATE, 1f);
|
||||
this.arena.toTribune(player);
|
||||
});
|
||||
this.afterCelebration();
|
||||
return;
|
||||
}
|
||||
|
||||
int loser = 1 - winner;
|
||||
boolean plural = this.teams.get(winner).size() > 1;
|
||||
Bukkit.broadcast(Style.message(
|
||||
this.teamNames(winner)
|
||||
.append(Component.text(plural ? " gewinnen gegen " : " hat gegen ", NamedTextColor.GRAY))
|
||||
.append(this.teamNames(loser))
|
||||
.append(Component.text(plural ? " " : " gewonnen ", NamedTextColor.GRAY))
|
||||
.append(detail)
|
||||
));
|
||||
|
||||
for(UUID uuid : this.teams.get(loser)) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player == null) continue;
|
||||
player.showTitle(Title.title(Component.text("Verloren", NamedTextColor.RED), Component.empty()));
|
||||
if(this.alive.get(loser).contains(uuid)) this.arena.toTribune(player);
|
||||
}
|
||||
for(UUID uuid : this.teams.get(winner)) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player == null) continue;
|
||||
player.showTitle(Title.title(
|
||||
Component.text("Gewonnen!", NamedTextColor.GOLD),
|
||||
Component.text("gegen ", NamedTextColor.GRAY).append(this.teamNames(loser))
|
||||
));
|
||||
Style.play(player, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f);
|
||||
if(this.alive.get(winner).contains(uuid)) this.celebrate(player.getLocation(), Style.PAD_PARTICLE_COLORS[winner]);
|
||||
}
|
||||
this.afterCelebration();
|
||||
}
|
||||
|
||||
private void celebrate(Location location, Color color) {
|
||||
Location top = location.clone().add(0, 2.2, 0);
|
||||
Particle.FIREWORK.builder().location(top).count(40).offset(0.6, 0.6, 0.6).extra(0.15).receivers(64).spawn();
|
||||
Particle.DUST.builder().location(top).count(60).offset(1, 1, 1).data(new Particle.DustOptions(color, 1.6f)).receivers(64).spawn();
|
||||
Style.play(top, Sound.ENTITY_FIREWORK_ROCKET_TWINKLE, 2f, 1f);
|
||||
Style.play(top, Sound.ENTITY_FIREWORK_ROCKET_LARGE_BLAST, 2f, 1f);
|
||||
}
|
||||
|
||||
private void hideBossBar() {
|
||||
this.bossBarViewers.forEach(uuid -> {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player != null) player.hideBossBar(this.bossBar);
|
||||
});
|
||||
this.bossBarViewers.clear();
|
||||
}
|
||||
|
||||
private void afterCelebration() {
|
||||
this.hideBossBar();
|
||||
Bukkit.getScheduler().runTaskLater(Main.instance(), () -> {
|
||||
for(Player player : this.alivePlayers()) {
|
||||
if(this.arena.fightOf(player) == this && this.arena.state(player) == Arena.PlayerState.FIGHT) this.arena.toTribune(player);
|
||||
}
|
||||
this.slot.fight = null;
|
||||
this.arena.resetSlot(this.slot);
|
||||
}, CELEBRATION_TICKS);
|
||||
}
|
||||
|
||||
private void stopTimer() {
|
||||
if(this.timer != null) this.timer.cancel();
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
private int teamOf(UUID player) {
|
||||
if(this.teams.get(0).contains(player)) return 0;
|
||||
if(this.teams.get(1).contains(player)) return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private List<Player> alivePlayers() {
|
||||
List<Player> players = new ArrayList<>();
|
||||
for(Set<UUID> team : this.alive) {
|
||||
for(UUID uuid : team) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player != null) players.add(player);
|
||||
}
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import io.papermc.paper.datacomponent.DataComponentTypes;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
/**
|
||||
* Lethal damage ends the fight instead of killing the player. Totems still work as usual.
|
||||
*/
|
||||
class FightListener extends ApplianceListener<Arena> {
|
||||
private boolean isFighting(Player player) {
|
||||
Fight fight = this.getAppliance().fightOf(player);
|
||||
return fight != null && !fight.isOver() && fight.isAlive(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Only fighters hit each other, and never their own team directly. Explosions still hurt everyone.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onDamageByPlayer(EntityDamageByEntityEvent event) {
|
||||
if(!(event.getEntity() instanceof Player victim)) return;
|
||||
Player attacker = switch(event.getDamager()) {
|
||||
case Player player -> player;
|
||||
case Projectile projectile when projectile.getShooter() instanceof Player shooter -> shooter;
|
||||
default -> null;
|
||||
};
|
||||
if(attacker == null || attacker == victim) return;
|
||||
Fight fight = this.getAppliance().fightOf(attacker);
|
||||
if(!this.isFighting(attacker) || fight.sameTeam(attacker.getUniqueId(), victim.getUniqueId())) event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDamage(EntityDamageEvent event) {
|
||||
if(!(event.getEntity() instanceof Player player)) return;
|
||||
if(this.getAppliance().state(player) != Arena.PlayerState.FIGHT) return;
|
||||
Fight fight = this.getAppliance().fightOf(player);
|
||||
if(fight == null || fight.isOver()) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
if(event.getFinalDamage() < player.getHealth()) return;
|
||||
if(this.hasDeathProtection(player.getInventory().getItemInMainHand())
|
||||
|| this.hasDeathProtection(player.getInventory().getItemInOffHand())) return;
|
||||
|
||||
event.setCancelled(true);
|
||||
fight.defeated(player);
|
||||
}
|
||||
|
||||
private boolean hasDeathProtection(ItemStack item) {
|
||||
return !item.isEmpty() && item.hasData(DataComponentTypes.DEATH_PROTECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safety net if a death gets through anyway: nothing drops and the fight ends.
|
||||
*/
|
||||
@EventHandler
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
event.setKeepInventory(true);
|
||||
event.getDrops().clear();
|
||||
event.setKeepLevel(true);
|
||||
event.setDroppedExp(0);
|
||||
event.deathMessage(null);
|
||||
Fight fight = this.getAppliance().fightOf(event.getPlayer());
|
||||
if(fight != null) fight.defeated(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
Bukkit.getScheduler().runTask(Main.instance(), () -> this.getAppliance().toTribune(event.getPlayer()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ender pearls and chorus fruits must not leave the arena.
|
||||
*/
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onTeleport(PlayerTeleportEvent event) {
|
||||
if(event.getCause() == PlayerTeleportEvent.TeleportCause.PLUGIN || event.getCause() == PlayerTeleportEvent.TeleportCause.COMMAND) return;
|
||||
if(this.getAppliance().state(event.getPlayer()) != Arena.PlayerState.FIGHT) return;
|
||||
Region region = this.getAppliance().arenaRegionOf(event.getPlayer());
|
||||
if(region == null) return;
|
||||
if(event.getTo().getWorld() != event.getFrom().getWorld() || !region.contains(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whoever ends up outside the box anyway (ender pearl through a wall or corner) loses.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
if(!event.hasChangedBlock()) return;
|
||||
Player player = event.getPlayer();
|
||||
if(this.getAppliance().state(player) != Arena.PlayerState.FIGHT || !this.isFighting(player)) return;
|
||||
Fight fight = this.getAppliance().fightOf(player);
|
||||
if(event.getTo().getWorld() == this.getAppliance().worlds().play() && fight.region().contains(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ())) return;
|
||||
fight.leftBox(player);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Inventory copies sent by the main server. Kept in memory only: after a restart of the arena server
|
||||
* players arrive with an empty inventory until they come over again.
|
||||
*/
|
||||
final class InventoryStore {
|
||||
static final int MAX_BYTES = 16 * 1024 * 1024;
|
||||
private static final Map<UUID, byte[]> snapshots = new ConcurrentHashMap<>();
|
||||
|
||||
private InventoryStore() {
|
||||
}
|
||||
|
||||
static void put(UUID player, byte[] serializedItems) {
|
||||
if(serializedItems.length > MAX_BYTES) throw new IllegalArgumentException("Inventory too large");
|
||||
snapshots.put(player, serializedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a fresh copy of the player's inventory, empty if none was sent
|
||||
*/
|
||||
static ItemStack[] get(UUID player) {
|
||||
byte[] bytes = snapshots.get(player);
|
||||
if(bytes == null) return new ItemStack[0];
|
||||
return ItemStack.deserializeItemsFromBytes(bytes);
|
||||
}
|
||||
|
||||
static void apply(Player player, UUID source) {
|
||||
ItemStack[] items = get(source);
|
||||
int size = player.getInventory().getSize();
|
||||
ItemStack[] contents = new ItemStack[size];
|
||||
System.arraycopy(items, 0, contents, 0, Math.min(items.length, size));
|
||||
player.getInventory().setContents(contents);
|
||||
player.setItemOnCursor(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.PadLobby;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
|
||||
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.TextDisplay;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* The two start pads in the world: who stands where, pushing others away from taken pads,
|
||||
* inventories, countdown, texts, particles and sounds. The rules themselves live in {@link PadLobby}.
|
||||
*/
|
||||
final class Lobby implements PadLobby.Listener {
|
||||
/** How far around a taken pad other players get pushed away. */
|
||||
private static final double PUSH_MARGIN = 0.4;
|
||||
/** How long a full lobby waits for the challenger to close the options. */
|
||||
private static final int CHOOSING_SECONDS = 20;
|
||||
/** Whoever keeps the options open this long without clicking anything gets sent off the pad. */
|
||||
private static final int MENU_IDLE_TICKS = 60 * 20;
|
||||
|
||||
private final Arena arena;
|
||||
private final Region[] regions;
|
||||
private final PadLobby pads;
|
||||
private final TextDisplay[] displays = new TextDisplay[PadLobby.PADS];
|
||||
private final Map<UUID, Long> lastPushSound = new HashMap<>();
|
||||
private long ticks;
|
||||
private @Nullable UUID menuViewer;
|
||||
private int menuIdleTicks;
|
||||
|
||||
Lobby(Arena arena, Region[] regions) {
|
||||
this.arena = arena;
|
||||
this.regions = regions;
|
||||
this.pads = new PadLobby(this, arena.settings().countdownSeconds(), CHOOSING_SECONDS);
|
||||
for(int i = 0; i < PadLobby.PADS; i++) {
|
||||
Region region = regions[i];
|
||||
this.displays[i] = Displays.spawn(new Location(arena.worlds().play(), region.centerX(), region.maxY() + 2.6, region.centerZ()));
|
||||
}
|
||||
}
|
||||
|
||||
void tick() {
|
||||
this.ticks++;
|
||||
for(Player player : this.arena.worlds().play().getPlayers()) {
|
||||
Arena.PlayerState state = this.arena.state(player);
|
||||
if(state == Arena.PlayerState.PAD) this.checkLeave(player);
|
||||
else if(state == Arena.PlayerState.TRIBUNE) this.checkEnter(player);
|
||||
}
|
||||
|
||||
this.checkMenuIdle();
|
||||
this.pads.tick(this.arena.hasFreeSlot());
|
||||
|
||||
if(this.ticks % 3 == 0) this.drawParticles();
|
||||
if(this.ticks % 5 == 0) this.updateDisplays();
|
||||
}
|
||||
|
||||
private void checkMenuIdle() {
|
||||
if(this.menuViewer == null || ++this.menuIdleTicks < MENU_IDLE_TICKS) return;
|
||||
Player player = Bukkit.getPlayer(this.menuViewer);
|
||||
this.menuViewer = null;
|
||||
if(player == null || !ModeMenu.isOpen(player)) return;
|
||||
this.arena.toTribune(player);
|
||||
player.sendMessage(Style.message("Du hast eine Minute lang nichts ausgewählt und wurdest vom Feld geschickt."));
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.6f);
|
||||
}
|
||||
|
||||
private void checkLeave(Player player) {
|
||||
int pad = this.pads.padOf(player.getUniqueId());
|
||||
if(pad != -1 && this.isOn(player, pad, 0)) return;
|
||||
this.leave(player);
|
||||
}
|
||||
|
||||
private void checkEnter(Player player) {
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) {
|
||||
if(this.pads.isPadFull(pad)) {
|
||||
if(this.isOn(player, pad, PUSH_MARGIN)) this.pushAway(player, pad);
|
||||
continue;
|
||||
}
|
||||
if(!this.isOn(player, pad, 0)) continue;
|
||||
if(!this.pads.enter(pad, player.getUniqueId())) continue;
|
||||
|
||||
this.arena.setState(player, Arena.PlayerState.PAD);
|
||||
this.arena.teams().join(player, Teams.pad(pad));
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 1.4f);
|
||||
if(this.pads.isWaitingForChallenger()) {
|
||||
player.sendActionBar(this.choosingText());
|
||||
} else if(!player.getUniqueId().equals(this.pads.challenger())) {
|
||||
player.sendActionBar(Component.text(Style.PAD_NAMES[pad] + " " + this.pads.players(pad).size() + "/" + this.pads.size(pad)
|
||||
+ ", Modus: " + this.modeName(), Style.PAD_TEXT_COLORS[pad]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the player from the pad, if standing on one.
|
||||
*/
|
||||
void leave(Player player) {
|
||||
if(this.pads.padOf(player.getUniqueId()) == -1) return;
|
||||
this.pads.leave(player.getUniqueId());
|
||||
if(ModeMenu.isOpen(player)) player.closeInventory();
|
||||
this.arena.teams().leave(player);
|
||||
this.arena.setState(player, Arena.PlayerState.TRIBUNE);
|
||||
}
|
||||
|
||||
void handleMenuClick(Player player, ModeMenu menu, ModeMenu.Click click) {
|
||||
int ownSize = menu.ownSize;
|
||||
int opponentSize = menu.opponentSize;
|
||||
FightMode mode = menu.mode;
|
||||
this.menuIdleTicks = 0;
|
||||
switch(click) {
|
||||
case ModeMenu.OwnSize(int size) -> ownSize = size;
|
||||
case ModeMenu.OpponentSize(int size) -> opponentSize = size;
|
||||
case ModeMenu.Mode(FightMode chosen) -> mode = chosen;
|
||||
case ModeMenu.Done() -> {
|
||||
InteractSounds.of(player).success();
|
||||
player.closeInventory();
|
||||
player.sendActionBar(Component.text(this.pads.size(this.pads.challengerPad()) + " gegen " + this.pads.size(1 - this.pads.challengerPad())
|
||||
+ ", Modus: " + this.modeName() + ". Rechtsklick öffnet die Auswahl wieder.", NamedTextColor.GOLD));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// switching to teams drops the opponent's inventory mode
|
||||
if(!mode.isAllowed(ownSize, opponentSize)) mode = FightMode.OWN;
|
||||
|
||||
int ownPad = this.pads.challengerPad();
|
||||
String problem = switch(this.pads.configure(player.getUniqueId(), ownSize, opponentSize, mode)) {
|
||||
case OK -> null;
|
||||
case NOT_ALLOWED -> "Der Countdown läuft schon";
|
||||
case OWN_TOO_SMALL -> "Auf deinem Feld stehen schon " + this.pads.players(ownPad).size();
|
||||
case OPPONENT_TOO_SMALL -> "Auf dem anderen Feld stehen schon " + this.pads.players(1 - ownPad).size();
|
||||
case INVALID -> "Das geht so nicht";
|
||||
};
|
||||
if(problem != null) {
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.6f);
|
||||
player.sendActionBar(Component.text(problem, NamedTextColor.RED));
|
||||
if(!this.pads.canConfigure(player.getUniqueId())) player.closeInventory();
|
||||
return;
|
||||
}
|
||||
menu.ownSize = ownSize;
|
||||
menu.opponentSize = opponentSize;
|
||||
menu.mode = mode;
|
||||
menu.draw();
|
||||
InteractSounds.of(player).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Right click on the pad: the challenger opens the options again.
|
||||
*/
|
||||
boolean openMenu(Player player) {
|
||||
if(!this.pads.canConfigure(player.getUniqueId()) || ModeMenu.isOpen(player)) return false;
|
||||
int pad = this.pads.challengerPad();
|
||||
InteractSounds.of(player).open();
|
||||
ModeMenu.open(player, pad, this.pads.size(pad), this.pads.size(1 - pad), this.pads.mode());
|
||||
this.menuViewer = player.getUniqueId();
|
||||
this.menuIdleTicks = 0;
|
||||
this.pads.setChoosing(player.getUniqueId(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The challenger closed the options: a full lobby starts counting down now.
|
||||
*/
|
||||
void menuClosed(Player player) {
|
||||
if(player.getUniqueId().equals(this.menuViewer)) this.menuViewer = null;
|
||||
this.pads.setChoosing(player.getUniqueId(), false);
|
||||
}
|
||||
|
||||
private Component choosingText() {
|
||||
Player challenger = this.pads.challenger() == null ? null : Bukkit.getPlayer(this.pads.challenger());
|
||||
String name = challenger == null ? "Der Herausforderer" : challenger.getName();
|
||||
return Component.text(name + " wählt noch Teams und Modus", NamedTextColor.YELLOW);
|
||||
}
|
||||
|
||||
private boolean isTeamFight() {
|
||||
return this.pads.size(0) > 1 || this.pads.size(1) > 1;
|
||||
}
|
||||
|
||||
private String modeName() {
|
||||
return this.pads.mode().displayName(this.isTeamFight());
|
||||
}
|
||||
|
||||
boolean isIdle() {
|
||||
return this.pads.isIdle();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) this.playersOn(pad).forEach(this::leave);
|
||||
for(TextDisplay display : this.displays) display.remove();
|
||||
}
|
||||
|
||||
private boolean isOn(Player player, int pad, double margin) {
|
||||
Region region = this.regions[pad];
|
||||
return player.getX() >= region.minX() - margin && player.getX() < region.maxX() + 1 + margin
|
||||
&& player.getZ() >= region.minZ() - margin && player.getZ() < region.maxZ() + 1 + margin
|
||||
&& player.getY() >= region.minY() - 0.5 && player.getY() < region.maxY() + 2;
|
||||
}
|
||||
|
||||
private void pushAway(Player player, int pad) {
|
||||
Region region = this.regions[pad];
|
||||
Vector direction = new Vector(player.getX() - region.centerX(), 0, player.getZ() - region.centerZ());
|
||||
if(direction.lengthSquared() < 0.01) direction = new Vector(1, 0, 0);
|
||||
player.setVelocity(direction.normalize().multiply(0.6).setY(0.25));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if(now - this.lastPushSound.getOrDefault(player.getUniqueId(), 0L) < 1000) return;
|
||||
this.lastPushSound.put(player.getUniqueId(), now);
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.6f);
|
||||
player.sendActionBar(Component.text("Dieses Feld ist voll", NamedTextColor.RED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void challengerSelected(UUID challenger, boolean first) {
|
||||
Player player = Bukkit.getPlayer(challenger);
|
||||
if(player == null) return;
|
||||
if(!first) {
|
||||
player.sendMessage(Style.message("Du führst jetzt die Herausforderung an. Rechtsklick auf dem Feld öffnet die Auswahl."));
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 1.8f);
|
||||
return;
|
||||
}
|
||||
player.sendMessage(Style.message("Du forderst heraus. Wähle Teamgröße und Modus, Rechtsklick auf dem Feld öffnet die Auswahl wieder. Wer sich auf die Felder stellt, macht mit."));
|
||||
this.openMenu(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void waitingForChallenger() {
|
||||
UUID challenger = this.pads.challenger();
|
||||
Player player = challenger == null ? null : Bukkit.getPlayer(challenger);
|
||||
if(player != null && player.getOpenInventory().getTopInventory().getHolder(false) instanceof ModeMenu menu) {
|
||||
menu.everyoneThere = true;
|
||||
menu.draw();
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 1.8f);
|
||||
}
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) {
|
||||
this.playersOn(pad).stream()
|
||||
.filter(occupant -> !occupant.getUniqueId().equals(challenger))
|
||||
.forEach(occupant -> occupant.sendActionBar(this.choosingText()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void countdownStarted() {
|
||||
UUID challenger = this.pads.challenger();
|
||||
Player player = challenger == null ? null : Bukkit.getPlayer(challenger);
|
||||
if(player != null && ModeMenu.isOpen(player)) player.closeInventory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kitAssigned(UUID uuid, UUID kitOwner) {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
if(player == null) return;
|
||||
InventoryStore.apply(player, kitOwner);
|
||||
if(uuid.equals(kitOwner)) return;
|
||||
Player owner = Bukkit.getPlayer(kitOwner);
|
||||
player.sendMessage(Style.message("Du kämpfst mit dem Inventar von " + (owner == null ? "jemand anderem" : owner.getName()) + ". Verlasse das Feld, um abzubrechen."));
|
||||
Style.play(player, Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void countdown(int secondsLeft) {
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) {
|
||||
Component opponents = this.namesOn(1 - pad);
|
||||
for(Player player : this.playersOn(pad)) {
|
||||
player.showTitle(Title.title(
|
||||
Component.text(secondsLeft, NamedTextColor.GOLD),
|
||||
Component.text("Kampf gegen ", NamedTextColor.GRAY).append(opponents),
|
||||
Title.Times.times(Duration.ZERO, Duration.ofMillis(1100), Duration.ZERO)
|
||||
));
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 0.6f + (this.arena.settings().countdownSeconds() - secondsLeft) * 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void countdownCancelled() {
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) {
|
||||
for(Player player : this.playersOn(pad)) {
|
||||
player.clearTitle();
|
||||
player.sendActionBar(Component.text("Abgebrochen", NamedTextColor.RED));
|
||||
Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(List<List<UUID>> teams, UUID challenger, FightMode mode, Map<UUID, UUID> kits) {
|
||||
List<List<Player>> players = teams.stream()
|
||||
.map(team -> team.stream().map(Bukkit::getPlayer).filter(Objects::nonNull).toList())
|
||||
.toList();
|
||||
if(players.get(0).isEmpty() || players.get(1).isEmpty()) {
|
||||
players.forEach(team -> team.forEach(this.arena::toTribune));
|
||||
return;
|
||||
}
|
||||
this.arena.startFight(players, mode, kits);
|
||||
}
|
||||
|
||||
private List<Player> playersOn(int pad) {
|
||||
return this.pads.players(pad).stream().map(Bukkit::getPlayer).filter(Objects::nonNull).toList();
|
||||
}
|
||||
|
||||
private Component namesOn(int pad) {
|
||||
return Component.join(
|
||||
JoinConfiguration.separator(Component.text(", ", NamedTextColor.GRAY)),
|
||||
this.playersOn(pad).stream().map(player -> Component.text(player.getName(), Style.PAD_TEXT_COLORS[pad])).toList()
|
||||
);
|
||||
}
|
||||
|
||||
private void updateDisplays() {
|
||||
boolean waiting = this.pads.isFull() && !this.arena.hasFreeSlot();
|
||||
Player challenger = this.pads.challenger() == null ? null : Bukkit.getPlayer(this.pads.challenger());
|
||||
int challengerPad = this.pads.challengerPad();
|
||||
String modeText = "Modus: " + this.modeName();
|
||||
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) {
|
||||
List<Player> players = this.playersOn(pad);
|
||||
Component title = Component.text(Style.PAD_NAMES[pad], Style.PAD_TEXT_COLORS[pad]);
|
||||
if(challenger != null) title = title.append(Component.text(" " + players.size() + "/" + this.pads.size(pad), NamedTextColor.WHITE));
|
||||
|
||||
Component body;
|
||||
if(waiting) {
|
||||
body = this.namesOn(pad).appendNewline()
|
||||
.append(Component.text("Alle Arenen belegt, gleich geht es los", NamedTextColor.GRAY));
|
||||
} else if(this.pads.isWaitingForChallenger() && challenger != null) {
|
||||
body = this.namesOn(pad).appendNewline()
|
||||
.append(Component.text(challenger.getName() + " wählt noch", NamedTextColor.YELLOW));
|
||||
} else if(this.pads.isCountingDown()) {
|
||||
body = this.namesOn(pad).appendNewline()
|
||||
.append(Component.text("Kampf beginnt in " + this.pads.secondsLeft(), NamedTextColor.GOLD));
|
||||
} else if(challenger == null) {
|
||||
body = !this.arena.hasFreeSlot()
|
||||
? Component.text("Alle Arenen belegt", NamedTextColor.GRAY).appendNewline()
|
||||
.append(Component.text("Du kannst dich trotzdem anstellen", NamedTextColor.WHITE))
|
||||
: Component.text("Stell dich hierhin,", NamedTextColor.WHITE).appendNewline()
|
||||
.append(Component.text("um herauszufordern", NamedTextColor.WHITE));
|
||||
} else if(pad == challengerPad) {
|
||||
body = this.namesOn(pad).appendNewline()
|
||||
.append(Component.text(players.size() == 1 ? "fordert heraus" : "fordern heraus", NamedTextColor.GRAY)).appendNewline()
|
||||
.append(Component.text(modeText, NamedTextColor.WHITE));
|
||||
} else {
|
||||
Component who = players.isEmpty()
|
||||
? Component.text("Stell dich hierhin, um gegen", NamedTextColor.WHITE).appendNewline()
|
||||
.append(Component.text(challenger.getName(), Style.PAD_TEXT_COLORS[challengerPad]))
|
||||
.append(Component.text(" zu kämpfen", NamedTextColor.WHITE))
|
||||
: this.namesOn(pad).appendNewline().append(Component.text("nehmen an", NamedTextColor.GRAY));
|
||||
body = who.appendNewline().append(Component.text(modeText, NamedTextColor.GRAY));
|
||||
}
|
||||
this.displays[pad].text(title.appendNewline().append(body));
|
||||
}
|
||||
}
|
||||
|
||||
private void drawParticles() {
|
||||
boolean waiting = this.pads.isFull() && !this.arena.hasFreeSlot();
|
||||
boolean countdown = this.pads.isCountingDown() && !waiting;
|
||||
for(int pad = 0; pad < PadLobby.PADS; pad++) {
|
||||
Region region = this.regions[pad];
|
||||
Color color = waiting ? Style.WAITING_PARTICLE_COLOR : Style.PAD_PARTICLE_COLORS[pad];
|
||||
boolean full = this.pads.isPadFull(pad);
|
||||
double spacing = countdown ? 0.25 : full ? 0.4 : 0.6;
|
||||
this.drawOutline(region, new Particle.DustOptions(color, full ? 1.2f : 0.9f), spacing);
|
||||
|
||||
double speed = countdown ? 0.6 : 0.3;
|
||||
for(Player occupant : this.playersOn(pad)) {
|
||||
for(int strand = 0; strand < 2; strand++) {
|
||||
double angle = this.ticks * speed + strand * Math.PI;
|
||||
double height = (this.ticks % 40) / 20.0;
|
||||
Location point = occupant.getLocation().add(Math.cos(angle) * 0.7, height, Math.sin(angle) * 0.7);
|
||||
Particle.DUST.builder().location(point).data(new Particle.DustOptions(color, 1f)).receivers(48).spawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawOutline(Region region, Particle.DustOptions dust, double spacing) {
|
||||
double y = region.minY() + 0.1;
|
||||
double minX = region.minX();
|
||||
double maxX = region.maxX() + 1;
|
||||
double minZ = region.minZ();
|
||||
double maxZ = region.maxZ() + 1;
|
||||
Location location = new Location(this.arena.worlds().play(), 0, y, 0);
|
||||
for(double x = minX; x <= maxX; x += spacing) {
|
||||
this.dust(location.set(x, y, minZ), dust);
|
||||
this.dust(location.set(x, y, maxZ), dust);
|
||||
}
|
||||
for(double z = minZ; z <= maxZ; z += spacing) {
|
||||
this.dust(location.set(minX, y, z), dust);
|
||||
this.dust(location.set(maxX, y, z), dust);
|
||||
}
|
||||
}
|
||||
|
||||
private void dust(Location location, Particle.DustOptions dust) {
|
||||
Particle.DUST.builder().location(location).data(dust).receivers(48).spawn();
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.EntityToggleGlideEvent;
|
||||
import org.bukkit.event.entity.FoodLevelChangeEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
|
||||
|
||||
/**
|
||||
* Outside of a fight the inventory can be looked at, but nothing can be moved, dropped or used.
|
||||
* Nobody takes damage or gets hungry on the tribune.
|
||||
*/
|
||||
class LockListener extends ApplianceListener<Arena> {
|
||||
private boolean isLocked(Object entity) {
|
||||
if(!(entity instanceof Player player)) return false;
|
||||
Arena.PlayerState state = this.getAppliance().state(player);
|
||||
return state == Arena.PlayerState.TRIBUNE || state == Arena.PlayerState.PAD;
|
||||
}
|
||||
|
||||
private void cancelIfLocked(Object entity, Cancellable event) {
|
||||
if(this.isLocked(entity)) event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onClick(InventoryClickEvent event) {
|
||||
this.cancelIfLocked(event.getWhoClicked(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onDrag(InventoryDragEvent event) {
|
||||
this.cancelIfLocked(event.getWhoClicked(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onDrop(PlayerDropItemEvent event) {
|
||||
this.cancelIfLocked(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onSwapHands(PlayerSwapHandItemsEvent event) {
|
||||
this.cancelIfLocked(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
this.cancelIfLocked(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onConsume(PlayerItemConsumeEvent event) {
|
||||
this.cancelIfLocked(event.getPlayer(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onPickup(EntityPickupItemEvent event) {
|
||||
this.cancelIfLocked(event.getEntity(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onLaunch(ProjectileLaunchEvent event) {
|
||||
this.cancelIfLocked(event.getEntity().getShooter(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onGlide(EntityToggleGlideEvent event) {
|
||||
if(event.isGliding()) this.cancelIfLocked(event.getEntity(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onHunger(FoodLevelChangeEvent event) {
|
||||
this.cancelIfLocked(event.getEntity(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onDamage(EntityDamageEvent event) {
|
||||
this.cancelIfLocked(event.getEntity(), event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ender chests would keep items between fights.
|
||||
*/
|
||||
@EventHandler
|
||||
public void onOpen(InventoryOpenEvent event) {
|
||||
if(event.getInventory().getType() == InventoryType.ENDER_CHEST) event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
|
||||
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.PadLobby;
|
||||
import eu.mhsl.craftattack.spawn.core.util.inventory.ItemBuilder;
|
||||
import eu.mhsl.craftattack.spawn.core.util.inventory.PlaceholderItems;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Chest menu in which the challenger picks team sizes and mode. Every click applies at once.
|
||||
* Works for Bedrock players as well.
|
||||
*/
|
||||
final class ModeMenu implements InventoryHolder {
|
||||
private static final int OWN_ROW = 10;
|
||||
private static final int OPPONENT_ROW = 19;
|
||||
private static final int[] MODE_SLOTS = {29, 31, 33};
|
||||
private static final FightMode[] MODES = {FightMode.OWN, FightMode.CHALLENGER, FightMode.OPPONENT};
|
||||
private static final int DONE_SLOT = 40;
|
||||
|
||||
sealed interface Click {
|
||||
}
|
||||
|
||||
record OwnSize(int size) implements Click {
|
||||
}
|
||||
|
||||
record OpponentSize(int size) implements Click {
|
||||
}
|
||||
|
||||
record Mode(FightMode mode) implements Click {
|
||||
}
|
||||
|
||||
record Done() implements Click {
|
||||
}
|
||||
|
||||
private final Inventory inventory;
|
||||
private final Player challenger;
|
||||
private final int ownPad;
|
||||
int ownSize;
|
||||
int opponentSize;
|
||||
FightMode mode;
|
||||
/** Both pads are full, only the challenger closing the menu is missing. */
|
||||
boolean everyoneThere;
|
||||
|
||||
private ModeMenu(Player challenger, int ownPad, int ownSize, int opponentSize, FightMode mode) {
|
||||
this.challenger = challenger;
|
||||
this.ownPad = ownPad;
|
||||
this.ownSize = ownSize;
|
||||
this.opponentSize = opponentSize;
|
||||
this.mode = mode;
|
||||
this.inventory = Bukkit.createInventory(this, 45, Component.text("Teams und Kampfmodus"));
|
||||
this.draw();
|
||||
}
|
||||
|
||||
static void open(Player challenger, int ownPad, int ownSize, int opponentSize, FightMode mode) {
|
||||
challenger.openInventory(new ModeMenu(challenger, ownPad, ownSize, opponentSize, mode).getInventory());
|
||||
}
|
||||
|
||||
static boolean isOpen(Player player) {
|
||||
return player.getOpenInventory().getTopInventory().getHolder(false) instanceof ModeMenu;
|
||||
}
|
||||
|
||||
Click clickAt(int slot) {
|
||||
if(slot > OWN_ROW && slot <= OWN_ROW + PadLobby.MAX_TEAM_SIZE) return new OwnSize(slot - OWN_ROW);
|
||||
if(slot > OPPONENT_ROW && slot <= OPPONENT_ROW + PadLobby.MAX_TEAM_SIZE) return new OpponentSize(slot - OPPONENT_ROW);
|
||||
for(int i = 0; i < MODE_SLOTS.length; i++) {
|
||||
if(MODE_SLOTS[i] == slot) return new Mode(MODES[i]);
|
||||
}
|
||||
if(slot == DONE_SLOT) return new Done();
|
||||
return null;
|
||||
}
|
||||
|
||||
void draw() {
|
||||
for(int i = 0; i < this.inventory.getSize(); i++) this.inventory.setItem(i, PlaceholderItems.grayStainedGlassPane);
|
||||
int opponentPad = 1 - this.ownPad;
|
||||
boolean teams = this.ownSize > 1 || this.opponentSize > 1;
|
||||
|
||||
this.inventory.setItem(OWN_ROW, this.label(Material.PLAYER_HEAD, "Dein Team", this.ownPad, "So viele stehen mit dir auf dem " + Style.PAD_NAMES[this.ownPad] + ".")
|
||||
.withMeta(meta -> ((SkullMeta) meta).setOwningPlayer(this.challenger)).build());
|
||||
this.inventory.setItem(OPPONENT_ROW, this.label(Material.SKELETON_SKULL, "Gegner", opponentPad, "So viele stellen sich auf das " + Style.PAD_NAMES[opponentPad] + ".").build());
|
||||
for(int size = 1; size <= PadLobby.MAX_TEAM_SIZE; size++) {
|
||||
this.inventory.setItem(OWN_ROW + size, this.sizeItem(size, this.ownPad, size == this.ownSize));
|
||||
this.inventory.setItem(OPPONENT_ROW + size, this.sizeItem(size, opponentPad, size == this.opponentSize));
|
||||
}
|
||||
|
||||
for(int i = 0; i < MODES.length; i++) {
|
||||
FightMode mode = MODES[i];
|
||||
this.inventory.setItem(MODE_SLOTS[i], this.modeItem(mode, teams, mode == this.mode, mode.isAllowed(this.ownSize, this.opponentSize)));
|
||||
}
|
||||
|
||||
ItemBuilder done = ItemBuilder.of(Material.LIME_CONCRETE)
|
||||
.displayName(Component.text(this.everyoneThere ? "Fertig, Kampf starten" : "Fertig", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false))
|
||||
.lore(this.ownSize + " gegen " + this.opponentSize + ", " + this.mode.displayName(teams) + ". "
|
||||
+ (this.everyoneThere ? "Alle sind da, der Countdown startet, sobald du fertig bist." : "Jetzt fehlen nur noch die anderen auf den Feldern."));
|
||||
if(this.everyoneThere) done.glint();
|
||||
this.inventory.setItem(DONE_SLOT, done.build());
|
||||
}
|
||||
|
||||
private ItemBuilder label(Material material, String name, int pad, String text) {
|
||||
return ItemBuilder.of(material)
|
||||
.displayName(Component.text(name, Style.PAD_TEXT_COLORS[pad]).decoration(TextDecoration.ITALIC, false))
|
||||
.lore(text);
|
||||
}
|
||||
|
||||
private ItemStack sizeItem(int size, int pad, boolean selected) {
|
||||
ItemBuilder builder = ItemBuilder.of(pad == 0 ? Material.RED_CONCRETE : Material.BLUE_CONCRETE)
|
||||
.amount(size)
|
||||
.displayName(Component.text(size + " Spieler", Style.PAD_TEXT_COLORS[pad]).decoration(TextDecoration.ITALIC, false));
|
||||
builder.lore(selected ? "Ausgewählt" : "Klicken zum Auswählen", selected ? NamedTextColor.GREEN : NamedTextColor.YELLOW);
|
||||
if(selected) builder.glint();
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ItemStack modeItem(FightMode mode, boolean teams, boolean selected, boolean allowed) {
|
||||
ItemBuilder builder = switch(mode) {
|
||||
case OWN -> ItemBuilder.of(Material.IRON_SWORD)
|
||||
.lore("Jeder kämpft mit seinem eigenen Inventar.");
|
||||
case CHALLENGER -> ItemBuilder.of(Material.PLAYER_HEAD)
|
||||
.withMeta(meta -> ((SkullMeta) meta).setOwningPlayer(this.challenger))
|
||||
.lore(teams ? "Alle kämpfen mit deinem Inventar." : "Ihr kämpft beide mit deinem Inventar.");
|
||||
case OPPONENT -> ItemBuilder.of(allowed ? Material.CHEST : Material.BARRIER)
|
||||
.lore(allowed
|
||||
? "Ihr kämpft beide mit dem Inventar von dem, der sich auf das andere Feld stellt."
|
||||
: "Geht nur bei 1 gegen 1.");
|
||||
};
|
||||
builder.displayName(Component.text(mode.displayName(teams), allowed ? NamedTextColor.GOLD : NamedTextColor.DARK_GRAY).decoration(TextDecoration.ITALIC, false));
|
||||
if(!allowed) return builder.build();
|
||||
builder.appendLore(Component.empty());
|
||||
if(selected) {
|
||||
builder.glint().appendLore(Component.text("Ausgewählt", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false));
|
||||
} else {
|
||||
builder.appendLore(Component.text("Klicken zum Auswählen", NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Inventory getInventory() {
|
||||
return this.inventory;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
|
||||
class ModeMenuListener extends ApplianceListener<Arena> {
|
||||
@EventHandler
|
||||
public void onClick(InventoryClickEvent event) {
|
||||
if(!(event.getView().getTopInventory().getHolder(false) instanceof ModeMenu menu)) return;
|
||||
event.setCancelled(true);
|
||||
if(!(event.getWhoClicked() instanceof Player player)) return;
|
||||
if(event.getRawSlot() >= event.getView().getTopInventory().getSize()) return;
|
||||
|
||||
ModeMenu.Click click = menu.clickAt(event.getRawSlot());
|
||||
Lobby lobby = this.getAppliance().lobby();
|
||||
if(click != null && lobby != null) lobby.handleMenuClick(player, menu, click);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onClose(InventoryCloseEvent event) {
|
||||
if(!(event.getInventory().getHolder(false) instanceof ModeMenu)) return;
|
||||
if(!(event.getPlayer() instanceof Player player)) return;
|
||||
Lobby lobby = this.getAppliance().lobby();
|
||||
if(lobby != null) lobby.menuClosed(player);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRightClick(PlayerInteractEvent event) {
|
||||
if(event.getHand() != EquipmentSlot.HAND) return;
|
||||
if(event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
|
||||
if(this.getAppliance().state(event.getPlayer()) != Arena.PlayerState.PAD) return;
|
||||
Lobby lobby = this.getAppliance().lobby();
|
||||
if(lobby != null && lobby.openMenu(event.getPlayer())) event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import com.destroystokyo.paper.event.player.PlayerAdvancementCriterionGrantEvent;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerRecipeDiscoverEvent;
|
||||
|
||||
/**
|
||||
* Nothing done in the arena counts as progress: no advancements, no recipe unlocks.
|
||||
*/
|
||||
class NoProgressListener extends ApplianceListener<Arena> {
|
||||
@EventHandler
|
||||
public void onCriterion(PlayerAdvancementCriterionGrantEvent event) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRecipe(PlayerRecipeDiscoverEvent event) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
class PlayerConnectionListener extends ApplianceListener<Arena> {
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
event.getPlayer().getEnderChest().clear();
|
||||
this.getAppliance().toTribune(event.getPlayer());
|
||||
event.getPlayer().sendMessage(Style.message(
|
||||
"Willkommen in der Arena! Stell dich auf ein Startfeld, um jemanden herauszufordern. Zurück geht es beim Villager oder mit /craftattack."
|
||||
));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
this.getAppliance().handleQuit(event.getPlayer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Colors, texts and sounds of the arena, so everything looks and sounds the same everywhere.
|
||||
* Player facing texts use no dashes.
|
||||
*/
|
||||
final class Style {
|
||||
static final Component PREFIX = Component.text("[Arena] ", NamedTextColor.GOLD);
|
||||
|
||||
static final NamedTextColor[] PAD_TEXT_COLORS = {NamedTextColor.RED, NamedTextColor.BLUE};
|
||||
static final Color[] PAD_PARTICLE_COLORS = {Color.fromRGB(255, 70, 70), Color.fromRGB(80, 120, 255)};
|
||||
static final String[] PAD_NAMES = {"Rotes Feld", "Blaues Feld"};
|
||||
static final Color WAITING_PARTICLE_COLOR = Color.fromRGB(150, 150, 150);
|
||||
|
||||
private Style() {
|
||||
}
|
||||
|
||||
static Component message(String text) {
|
||||
return PREFIX.append(Component.text(text, NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
static Component message(Component text) {
|
||||
return PREFIX.append(text.colorIfAbsent(NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
static Component name(String name, TextColor color) {
|
||||
return Component.text(name, color);
|
||||
}
|
||||
|
||||
static String hearts(double health) {
|
||||
return String.format(Locale.GERMAN, "%.1f", health / 2);
|
||||
}
|
||||
|
||||
static String time(int seconds) {
|
||||
return String.format("%d:%02d", seconds / 60, seconds % 60);
|
||||
}
|
||||
|
||||
static void play(Player player, Sound sound, float pitch) {
|
||||
player.playSound(player, sound, 1f, pitch);
|
||||
}
|
||||
|
||||
static void play(Location location, Sound sound, float volume, float pitch) {
|
||||
location.getWorld().playSound(location, sound, volume, pitch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.bukkit.scoreboard.Team;
|
||||
|
||||
/**
|
||||
* Scoreboard teams for the colored names. Players on a pad cannot be pushed (no collision),
|
||||
* on the tribune and in a fight collision stays on.
|
||||
*/
|
||||
final class Teams {
|
||||
private static final String[] NAMES = {"arena_pad_red", "arena_pad_blue", "arena_fight_red", "arena_fight_blue"};
|
||||
|
||||
private final Team[] teams = new Team[NAMES.length];
|
||||
|
||||
Teams() {
|
||||
Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
|
||||
for(int i = 0; i < NAMES.length; i++) {
|
||||
Team team = scoreboard.getTeam(NAMES[i]);
|
||||
if(team == null) team = scoreboard.registerNewTeam(NAMES[i]);
|
||||
team.color(Style.PAD_TEXT_COLORS[i % 2]);
|
||||
team.setOption(Team.Option.COLLISION_RULE, i < 2 ? Team.OptionStatus.NEVER : Team.OptionStatus.ALWAYS);
|
||||
this.teams[i] = team;
|
||||
}
|
||||
}
|
||||
|
||||
static int pad(int pad) {
|
||||
return pad;
|
||||
}
|
||||
|
||||
static int fight(int side) {
|
||||
return 2 + side;
|
||||
}
|
||||
|
||||
void join(Player player, int team) {
|
||||
this.leave(player);
|
||||
this.teams[team].addEntry(player.getName());
|
||||
}
|
||||
|
||||
void leave(Player player) {
|
||||
for(Team team : this.teams) team.removeEntry(player.getName());
|
||||
}
|
||||
|
||||
void clear() {
|
||||
for(Team team : this.teams) team.getEntries().forEach(team::removeEntry);
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockBurnEvent;
|
||||
import org.bukkit.event.block.BlockExplodeEvent;
|
||||
import org.bukkit.event.block.BlockFadeEvent;
|
||||
import org.bukkit.event.block.BlockFormEvent;
|
||||
import org.bukkit.event.block.BlockFromToEvent;
|
||||
import org.bukkit.event.block.BlockIgniteEvent;
|
||||
import org.bukkit.event.block.BlockPistonExtendEvent;
|
||||
import org.bukkit.event.block.BlockPistonRetractEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.block.BlockSpreadEvent;
|
||||
import org.bukkit.event.block.LeavesDecayEvent;
|
||||
import org.bukkit.event.entity.EntityChangeBlockEvent;
|
||||
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||
import org.bukkit.event.hanging.HangingPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerBucketEmptyEvent;
|
||||
import org.bukkit.event.player.PlayerBucketFillEvent;
|
||||
import org.bukkit.event.world.StructureGrowEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* In the play world blocks may only change inside the arenas. Everything else (tribune, pads, the space
|
||||
* between arenas) stays exactly as in the template.
|
||||
*/
|
||||
class WorldProtectionListener extends ApplianceListener<Arena> {
|
||||
private boolean isProtected(Block block) {
|
||||
return this.isProtected(block.getLocation());
|
||||
}
|
||||
|
||||
private boolean isProtected(Location location) {
|
||||
if(!this.getAppliance().worlds().isPlay(location.getWorld())) return false;
|
||||
return !this.getAppliance().isInsideArena(location);
|
||||
}
|
||||
|
||||
private void cancelIfProtected(Block block, Cancellable event) {
|
||||
if(this.isProtected(block)) event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onPlace(BlockPlaceEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onBreak(BlockBreakEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onBucketEmpty(PlayerBucketEmptyEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onBucketFill(PlayerBucketFillEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onFlow(BlockFromToEvent event) {
|
||||
this.cancelIfProtected(event.getToBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onSpread(BlockSpreadEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onForm(BlockFormEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onFade(BlockFadeEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onIgnite(BlockIgniteEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onBurn(BlockBurnEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onLeavesDecay(LeavesDecayEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onEntityChangeBlock(EntityChangeBlockEvent event) {
|
||||
this.cancelIfProtected(event.getBlock(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onHangingPlace(HangingPlaceEvent event) {
|
||||
this.cancelIfProtected(event.getBlock().getRelative(event.getBlockFace()), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onGrow(StructureGrowEvent event) {
|
||||
if(!this.getAppliance().worlds().isPlay(event.getWorld())) return;
|
||||
event.getBlocks().removeIf(state -> this.isProtected(state.getLocation()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onPistonExtend(BlockPistonExtendEvent event) {
|
||||
if(this.anyProtected(event.getBlock(), event.getBlocks(), event.getDirection().getModX(), event.getDirection().getModY(), event.getDirection().getModZ()))
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onPistonRetract(BlockPistonRetractEvent event) {
|
||||
if(this.anyProtected(event.getBlock(), event.getBlocks(), event.getDirection().getModX(), event.getDirection().getModY(), event.getDirection().getModZ()))
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
private boolean anyProtected(Block piston, List<Block> moved, int dx, int dy, int dz) {
|
||||
if(this.isProtected(piston) || this.isProtected(piston.getRelative(dx, dy, dz))) return true;
|
||||
return moved.stream().anyMatch(block -> this.isProtected(block) || this.isProtected(block.getRelative(dx, dy, dz)));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onEntityExplode(EntityExplodeEvent event) {
|
||||
event.blockList().removeIf(this::isProtected);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onBlockExplode(BlockExplodeEvent event) {
|
||||
event.blockList().removeIf(this::isProtected);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
/**
|
||||
* Which inventory the fighters use. The challenger chooses it.
|
||||
*/
|
||||
public enum FightMode {
|
||||
OWN,
|
||||
CHALLENGER,
|
||||
OPPONENT;
|
||||
|
||||
/**
|
||||
* With more than one player on a side, "the opponent's inventory" would be ambiguous.
|
||||
*/
|
||||
public boolean isAllowed(int ownSize, int opponentSize) {
|
||||
return this != OPPONENT || (ownSize == 1 && opponentSize == 1);
|
||||
}
|
||||
|
||||
public String displayName(boolean teams) {
|
||||
return switch(this) {
|
||||
case OWN -> "Jeder sein eigenes";
|
||||
case CHALLENGER -> teams ? "Alle mit meinem" : "Beide mit meinem";
|
||||
case OPPONENT -> "Beide mit dem des Gegners";
|
||||
};
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* State of the two start pads. The first player on a pad becomes the challenger and picks team sizes and mode.
|
||||
* Everyone else joins a team by stepping on a pad. Once both pads hold exactly the chosen number of players
|
||||
* and the challenger is done choosing (or took too long), the countdown runs and then the fight starts.
|
||||
*/
|
||||
public class PadLobby {
|
||||
public static final int PADS = 2;
|
||||
public static final int MAX_TEAM_SIZE = 4;
|
||||
|
||||
public interface Listener {
|
||||
/** The player is now the challenger and may change the options until the countdown runs. */
|
||||
void challengerSelected(UUID challenger, boolean first);
|
||||
|
||||
/** The player should now hold the inventory of {@code kitOwner} (the own one to revert). */
|
||||
void kitAssigned(UUID player, UUID kitOwner);
|
||||
|
||||
/** Both pads are exactly full, but the challenger still has the options open. */
|
||||
void waitingForChallenger();
|
||||
|
||||
/** Both pads are exactly full, the options are fixed now. */
|
||||
void countdownStarted();
|
||||
|
||||
void countdown(int secondsLeft);
|
||||
|
||||
void countdownCancelled();
|
||||
|
||||
/**
|
||||
* @param teams players per pad
|
||||
* @param kits whose inventory each player fights with
|
||||
*/
|
||||
void start(List<List<UUID>> teams, UUID challenger, FightMode mode, Map<UUID, UUID> kits);
|
||||
}
|
||||
|
||||
private final Listener listener;
|
||||
private final int countdownTicks;
|
||||
private final int choosingTicks;
|
||||
private final List<List<UUID>> pads = List.of(new ArrayList<>(), new ArrayList<>());
|
||||
/** Everyone on a pad, in the order they stepped on. The first one is the challenger. */
|
||||
private final List<UUID> order = new ArrayList<>();
|
||||
private final Map<UUID, UUID> kits = new HashMap<>();
|
||||
private final int[] sizes = {1, 1};
|
||||
private FightMode mode = FightMode.OWN;
|
||||
private int ticksLeft = -1;
|
||||
private boolean choosing;
|
||||
/** How much longer everyone waits for the challenger to finish choosing; -1 while nobody waits. */
|
||||
private int choosingTicksLeft = -1;
|
||||
|
||||
/**
|
||||
* @param choosingSeconds how long a full lobby waits for the challenger to close the options
|
||||
*/
|
||||
public PadLobby(Listener listener, int countdownSeconds, int choosingSeconds) {
|
||||
this.listener = listener;
|
||||
this.countdownTicks = countdownSeconds * 20;
|
||||
this.choosingTicks = choosingSeconds * 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false if the pad is already full or the player stands on a pad
|
||||
*/
|
||||
public boolean enter(int pad, UUID player) {
|
||||
if(this.padOf(player) != -1) return false;
|
||||
boolean first = this.order.isEmpty();
|
||||
if(!first && this.pads.get(pad).size() >= this.sizes[pad]) return false;
|
||||
|
||||
this.pads.get(pad).add(player);
|
||||
this.order.add(player);
|
||||
this.kits.put(player, player);
|
||||
if(first) {
|
||||
this.sizes[0] = 1;
|
||||
this.sizes[1] = 1;
|
||||
this.mode = FightMode.OWN;
|
||||
this.listener.challengerSelected(player, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.refreshKits();
|
||||
this.startCountdownIfReady();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void leave(UUID player) {
|
||||
int pad = this.padOf(player);
|
||||
if(pad == -1) return;
|
||||
|
||||
boolean wasFull = this.isFull();
|
||||
boolean wasChallenger = player.equals(this.challenger());
|
||||
this.pads.get(pad).remove(player);
|
||||
this.order.remove(player);
|
||||
this.kits.remove(player);
|
||||
this.listener.kitAssigned(player, player);
|
||||
|
||||
if(wasFull) {
|
||||
if(this.isCountingDown()) this.listener.countdownCancelled();
|
||||
this.ticksLeft = -1;
|
||||
this.choosingTicksLeft = -1;
|
||||
}
|
||||
if(wasChallenger) this.choosing = false;
|
||||
|
||||
this.refreshKits();
|
||||
// the longest waiting player takes over; team sizes and mode stay as chosen
|
||||
if(wasChallenger && !this.order.isEmpty()) {
|
||||
this.listener.challengerSelected(this.order.getFirst(), false);
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConfigureResult {
|
||||
OK,
|
||||
NOT_ALLOWED,
|
||||
OWN_TOO_SMALL,
|
||||
OPPONENT_TOO_SMALL,
|
||||
INVALID
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the challenger can change the options, and only until the countdown runs.
|
||||
* Teams cannot be smaller than the players already standing there.
|
||||
*
|
||||
* @param ownSize players on the challenger's pad
|
||||
* @param opponentSize players on the other pad
|
||||
*/
|
||||
public ConfigureResult configure(UUID player, int ownSize, int opponentSize, FightMode mode) {
|
||||
if(!this.canConfigure(player)) return ConfigureResult.NOT_ALLOWED;
|
||||
if(ownSize < 1 || ownSize > MAX_TEAM_SIZE || opponentSize < 1 || opponentSize > MAX_TEAM_SIZE) return ConfigureResult.INVALID;
|
||||
if(!mode.isAllowed(ownSize, opponentSize)) return ConfigureResult.INVALID;
|
||||
int own = this.challengerPad();
|
||||
if(ownSize < this.pads.get(own).size()) return ConfigureResult.OWN_TOO_SMALL;
|
||||
if(opponentSize < this.pads.get(1 - own).size()) return ConfigureResult.OPPONENT_TOO_SMALL;
|
||||
|
||||
this.sizes[own] = ownSize;
|
||||
this.sizes[1 - own] = opponentSize;
|
||||
this.mode = mode;
|
||||
this.refreshKits();
|
||||
this.startCountdownIfReady();
|
||||
return ConfigureResult.OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* The challenger opened or closed the options. While they are open, a full lobby waits instead of counting down.
|
||||
*/
|
||||
public void setChoosing(UUID player, boolean choosing) {
|
||||
if(!this.canConfigure(player) || this.choosing == choosing) return;
|
||||
this.choosing = choosing;
|
||||
this.startCountdownIfReady();
|
||||
}
|
||||
|
||||
public boolean isWaitingForChallenger() {
|
||||
return this.choosingTicksLeft >= 0;
|
||||
}
|
||||
|
||||
private void startCountdownIfReady() {
|
||||
if(this.isCountingDown()) return;
|
||||
if(!this.isFull()) {
|
||||
this.choosingTicksLeft = -1;
|
||||
return;
|
||||
}
|
||||
if(this.choosing) {
|
||||
if(this.choosingTicksLeft >= 0) return;
|
||||
this.choosingTicksLeft = this.choosingTicks;
|
||||
this.listener.waitingForChallenger();
|
||||
return;
|
||||
}
|
||||
this.choosingTicksLeft = -1;
|
||||
this.ticksLeft = this.countdownTicks;
|
||||
this.listener.countdownStarted();
|
||||
}
|
||||
|
||||
public boolean canConfigure(UUID player) {
|
||||
return player.equals(this.challenger()) && !this.isCountingDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every server tick. The countdown only runs while an arena is free.
|
||||
*/
|
||||
public void tick(boolean arenaAvailable) {
|
||||
if(this.choosingTicksLeft >= 0 && this.choosingTicksLeft-- == 0) {
|
||||
this.choosing = false;
|
||||
this.startCountdownIfReady();
|
||||
}
|
||||
if(this.ticksLeft < 0) return;
|
||||
if(!arenaAvailable) {
|
||||
this.ticksLeft = this.countdownTicks;
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.ticksLeft % 20 == 0 && this.ticksLeft > 0) this.listener.countdown(this.ticksLeft / 20);
|
||||
if(this.ticksLeft-- > 0) return;
|
||||
|
||||
List<List<UUID>> teams = List.of(List.copyOf(this.pads.get(0)), List.copyOf(this.pads.get(1)));
|
||||
UUID challenger = this.challenger();
|
||||
FightMode mode = this.mode;
|
||||
Map<UUID, UUID> kits = Map.copyOf(this.kits);
|
||||
this.pads.forEach(List::clear);
|
||||
this.order.clear();
|
||||
this.kits.clear();
|
||||
this.sizes[0] = 1;
|
||||
this.sizes[1] = 1;
|
||||
this.mode = FightMode.OWN;
|
||||
this.ticksLeft = -1;
|
||||
this.choosing = false;
|
||||
this.listener.start(teams, challenger, mode, kits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Works out whose inventory everyone should hold and reports only the changes.
|
||||
*/
|
||||
private void refreshKits() {
|
||||
UUID challenger = this.challenger();
|
||||
if(challenger == null) return;
|
||||
int challengerPad = this.challengerPad();
|
||||
UUID firstOpponent = this.pads.get(1 - challengerPad).isEmpty() ? null : this.pads.get(1 - challengerPad).getFirst();
|
||||
|
||||
for(UUID player : this.order) {
|
||||
UUID wanted = switch(this.mode) {
|
||||
case OWN -> player;
|
||||
case CHALLENGER -> challenger;
|
||||
case OPPONENT -> player.equals(challenger) && firstOpponent != null ? firstOpponent : player;
|
||||
};
|
||||
if(wanted.equals(this.kits.get(player))) continue;
|
||||
this.kits.put(player, wanted);
|
||||
this.listener.kitAssigned(player, wanted);
|
||||
}
|
||||
}
|
||||
|
||||
public int padOf(UUID player) {
|
||||
for(int i = 0; i < PADS; i++) {
|
||||
if(this.pads.get(i).contains(player)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public List<UUID> players(int pad) {
|
||||
return List.copyOf(this.pads.get(pad));
|
||||
}
|
||||
|
||||
public int size(int pad) {
|
||||
return this.sizes[pad];
|
||||
}
|
||||
|
||||
public boolean isPadFull(int pad) {
|
||||
return !this.order.isEmpty() && this.pads.get(pad).size() >= this.sizes[pad];
|
||||
}
|
||||
|
||||
public @Nullable UUID challenger() {
|
||||
return this.order.isEmpty() ? null : this.order.getFirst();
|
||||
}
|
||||
|
||||
public int challengerPad() {
|
||||
UUID challenger = this.challenger();
|
||||
return challenger == null ? -1 : this.padOf(challenger);
|
||||
}
|
||||
|
||||
public FightMode mode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
public boolean isFull() {
|
||||
return this.pads.get(0).size() == this.sizes[0] && this.pads.get(1).size() == this.sizes[1];
|
||||
}
|
||||
|
||||
public boolean isCountingDown() {
|
||||
return this.ticksLeft >= 0;
|
||||
}
|
||||
|
||||
public boolean isIdle() {
|
||||
return this.order.isEmpty();
|
||||
}
|
||||
|
||||
/** Seconds until the start, rounded up; -1 without countdown. */
|
||||
public int secondsLeft() {
|
||||
return this.ticksLeft < 0 ? -1 : (this.ticksLeft + 19) / 20;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
/**
|
||||
* Axis-aligned box of whole blocks, both corners inclusive.
|
||||
*/
|
||||
public record Region(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
public static Region of(int x1, int y1, int z1, int x2, int y2, int z2) {
|
||||
return new Region(
|
||||
Math.min(x1, x2), Math.min(y1, y2), Math.min(z1, z2),
|
||||
Math.max(x1, x2), Math.max(y1, y2), Math.max(z1, z2)
|
||||
);
|
||||
}
|
||||
|
||||
public boolean contains(int x, int y, int z) {
|
||||
return x >= this.minX && x <= this.maxX
|
||||
&& y >= this.minY && y <= this.maxY
|
||||
&& z >= this.minZ && z <= this.maxZ;
|
||||
}
|
||||
|
||||
public boolean contains(double x, double y, double z) {
|
||||
return this.contains((int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z));
|
||||
}
|
||||
|
||||
public Region expand(int horizontal, int up) {
|
||||
return new Region(
|
||||
this.minX - horizontal, this.minY, this.minZ - horizontal,
|
||||
this.maxX + horizontal, this.maxY + up, this.maxZ + horizontal
|
||||
);
|
||||
}
|
||||
|
||||
public long volume() {
|
||||
return (long) (this.maxX - this.minX + 1) * (this.maxY - this.minY + 1) * (this.maxZ - this.minZ + 1);
|
||||
}
|
||||
|
||||
public double centerX() {
|
||||
return (this.minX + this.maxX + 1) / 2.0;
|
||||
}
|
||||
|
||||
public double centerZ() {
|
||||
return (this.minZ + this.maxZ + 1) / 2.0;
|
||||
}
|
||||
|
||||
public int minChunkX() {
|
||||
return this.minX >> 4;
|
||||
}
|
||||
|
||||
public int maxChunkX() {
|
||||
return this.maxX >> 4;
|
||||
}
|
||||
|
||||
public int minChunkZ() {
|
||||
return this.minZ >> 4;
|
||||
}
|
||||
|
||||
public int maxChunkZ() {
|
||||
return this.maxZ >> 4;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
/**
|
||||
* Walks through every block of a region in portions, so a reset can be spread over several ticks.
|
||||
*/
|
||||
public class ResetCursor {
|
||||
@FunctionalInterface
|
||||
public interface BlockVisitor {
|
||||
void visit(int x, int y, int z);
|
||||
}
|
||||
|
||||
private final Region region;
|
||||
private int x;
|
||||
private int y;
|
||||
private int z;
|
||||
private boolean done;
|
||||
|
||||
public ResetCursor(Region region) {
|
||||
this.region = region;
|
||||
this.x = region.minX();
|
||||
this.y = region.minY();
|
||||
this.z = region.minZ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits up to {@code budget} blocks, continuing where the last call stopped.
|
||||
*
|
||||
* @return number of visited blocks
|
||||
*/
|
||||
public int advance(int budget, BlockVisitor visitor) {
|
||||
int visited = 0;
|
||||
while(!this.done && visited < budget) {
|
||||
visitor.visit(this.x, this.y, this.z);
|
||||
visited++;
|
||||
this.step();
|
||||
}
|
||||
return visited;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return this.done;
|
||||
}
|
||||
|
||||
private void step() {
|
||||
if(++this.y <= this.region.maxY()) return;
|
||||
this.y = this.region.minY();
|
||||
if(++this.z <= this.region.maxZ()) return;
|
||||
this.z = this.region.minZ();
|
||||
if(++this.x <= this.region.maxX()) return;
|
||||
this.done = true;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class FightModeTest {
|
||||
@Test
|
||||
void opponentInventoryOnlyInOneVersusOne() {
|
||||
assertTrue(FightMode.OPPONENT.isAllowed(1, 1));
|
||||
assertFalse(FightMode.OPPONENT.isAllowed(1, 2));
|
||||
assertFalse(FightMode.OPPONENT.isAllowed(3, 1));
|
||||
assertTrue(FightMode.CHALLENGER.isAllowed(4, 3));
|
||||
assertTrue(FightMode.OWN.isAllowed(2, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
void namesForTeams() {
|
||||
assertEquals("Beide mit meinem", FightMode.CHALLENGER.displayName(false));
|
||||
assertEquals("Alle mit meinem", FightMode.CHALLENGER.displayName(true));
|
||||
}
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PadLobbyTest {
|
||||
private final UUID a = UUID.randomUUID();
|
||||
private final UUID b = UUID.randomUUID();
|
||||
private final UUID c = UUID.randomUUID();
|
||||
private final UUID d = UUID.randomUUID();
|
||||
private final UUID e = UUID.randomUUID();
|
||||
private final List<String> events = new ArrayList<>();
|
||||
private Map<UUID, UUID> startedKits;
|
||||
private PadLobby lobby;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.lobby = new PadLobby(new PadLobby.Listener() {
|
||||
public void challengerSelected(UUID challenger, boolean first) {
|
||||
PadLobbyTest.this.events.add("challenger " + PadLobbyTest.this.name(challenger) + (first ? " first" : ""));
|
||||
}
|
||||
|
||||
public void waitingForChallenger() {
|
||||
PadLobbyTest.this.events.add("waiting");
|
||||
}
|
||||
|
||||
public void countdownStarted() {
|
||||
PadLobbyTest.this.events.add("countdown started");
|
||||
}
|
||||
|
||||
public void kitAssigned(UUID player, UUID kitOwner) {
|
||||
PadLobbyTest.this.events.add("kit " + PadLobbyTest.this.name(player) + "<-" + PadLobbyTest.this.name(kitOwner));
|
||||
}
|
||||
|
||||
public void countdown(int secondsLeft) {
|
||||
PadLobbyTest.this.events.add("countdown " + secondsLeft);
|
||||
}
|
||||
|
||||
public void countdownCancelled() {
|
||||
PadLobbyTest.this.events.add("cancelled");
|
||||
}
|
||||
|
||||
public void start(List<List<UUID>> teams, UUID challenger, FightMode mode, Map<UUID, UUID> kits) {
|
||||
PadLobbyTest.this.startedKits = kits;
|
||||
PadLobbyTest.this.events.add("start " + PadLobbyTest.this.names(teams.get(0)) + " vs " + PadLobbyTest.this.names(teams.get(1))
|
||||
+ " by " + PadLobbyTest.this.name(challenger) + " " + mode);
|
||||
}
|
||||
}, 2, 3);
|
||||
}
|
||||
|
||||
private String name(UUID uuid) {
|
||||
if(uuid.equals(this.a)) return "a";
|
||||
if(uuid.equals(this.b)) return "b";
|
||||
if(uuid.equals(this.c)) return "c";
|
||||
if(uuid.equals(this.d)) return "d";
|
||||
return "e";
|
||||
}
|
||||
|
||||
private String names(List<UUID> uuids) {
|
||||
return uuids.stream().map(this::name).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
private void run(int ticks) {
|
||||
for(int i = 0; i < ticks; i++) this.lobby.tick(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstOnPadBecomesChallengerAndMayConfigure() {
|
||||
assertTrue(this.lobby.enter(1, this.a));
|
||||
assertEquals(List.of("challenger a first"), this.events);
|
||||
assertEquals(this.a, this.lobby.challenger());
|
||||
assertTrue(this.lobby.canConfigure(this.a));
|
||||
assertFalse(this.lobby.isCountingDown());
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneVersusOneStartsAfterCountdown() {
|
||||
this.lobby.enter(0, this.a);
|
||||
assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER));
|
||||
this.lobby.enter(1, this.b);
|
||||
assertEquals(PadLobby.ConfigureResult.NOT_ALLOWED, this.lobby.configure(this.a, 1, 1, FightMode.OWN), "options are fixed during the countdown");
|
||||
this.run(41);
|
||||
|
||||
assertEquals(List.of(
|
||||
"challenger a first",
|
||||
"kit b<-a",
|
||||
"countdown started",
|
||||
"countdown 2",
|
||||
"countdown 1",
|
||||
"start a vs b by a CHALLENGER"
|
||||
), this.events);
|
||||
assertEquals(Map.of(this.a, this.a, this.b, this.a), this.startedKits);
|
||||
assertTrue(this.lobby.isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullPadIsRefusedAndPlayerOnlyTakesOnePad() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.enter(1, this.b);
|
||||
assertFalse(this.lobby.enter(1, this.c), "1v1: the blue pad is full");
|
||||
assertFalse(this.lobby.enter(0, this.b), "a player can only stand on one pad");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unevenTeamsStartWhenBothPadsAreExactlyFull() {
|
||||
this.lobby.enter(0, this.a);
|
||||
assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 2, 3, FightMode.OWN));
|
||||
assertTrue(this.lobby.enter(0, this.b));
|
||||
assertFalse(this.lobby.enter(0, this.c), "red is full with 2");
|
||||
this.lobby.enter(1, this.c);
|
||||
this.lobby.enter(1, this.d);
|
||||
assertFalse(this.lobby.isCountingDown());
|
||||
this.lobby.enter(1, this.e);
|
||||
assertTrue(this.lobby.isCountingDown());
|
||||
this.run(41);
|
||||
assertEquals("start ab vs cde by a OWN", this.events.getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
void challengerKitGoesToEveryone() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.configure(this.a, 2, 2, FightMode.CHALLENGER);
|
||||
this.lobby.enter(0, this.b);
|
||||
this.lobby.enter(1, this.c);
|
||||
this.lobby.enter(1, this.d);
|
||||
assertEquals(List.of("challenger a first", "kit b<-a", "kit c<-a", "kit d<-a", "countdown started"), this.events);
|
||||
}
|
||||
|
||||
@Test
|
||||
void opponentModeOnlyForOneVersusOne() {
|
||||
this.lobby.enter(0, this.a);
|
||||
assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 2, 1, FightMode.OPPONENT));
|
||||
assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 1, FightMode.OPPONENT));
|
||||
this.lobby.enter(1, this.b);
|
||||
assertTrue(this.events.contains("kit a<-b"));
|
||||
|
||||
this.lobby.leave(this.b);
|
||||
assertTrue(this.events.contains("kit a<-a"), "challenger gets the own inventory back");
|
||||
assertTrue(this.events.contains("cancelled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyChallengerMayConfigure() {
|
||||
this.lobby.enter(0, this.a);
|
||||
assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.b, 2, 2, FightMode.OWN));
|
||||
assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 5, 1, FightMode.OWN));
|
||||
assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 0, 1, FightMode.OWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void countdownWaitsForFreeArena() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.enter(1, this.b);
|
||||
for(int i = 0; i < 100; i++) this.lobby.tick(false);
|
||||
assertFalse(this.events.contains("countdown 2"));
|
||||
assertEquals(2, this.lobby.secondsLeft());
|
||||
this.run(41);
|
||||
assertEquals("start a vs b by a OWN", this.events.getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavingCancelsCountdownAndRevertsKit() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.events.clear();
|
||||
|
||||
this.lobby.leave(this.b);
|
||||
|
||||
assertEquals(List.of("kit b<-b", "cancelled"), this.events);
|
||||
assertEquals(this.a, this.lobby.challenger());
|
||||
assertEquals(FightMode.CHALLENGER, this.lobby.mode());
|
||||
assertFalse(this.lobby.isCountingDown());
|
||||
}
|
||||
|
||||
@Test
|
||||
void longestWaitingTakesOverAndKeepsTeamSizes() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.configure(this.a, 2, 2, FightMode.CHALLENGER);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.lobby.enter(0, this.c);
|
||||
this.events.clear();
|
||||
|
||||
this.lobby.leave(this.a);
|
||||
|
||||
assertEquals(List.of("kit a<-a", "kit b<-b", "kit c<-b", "challenger b"), this.events);
|
||||
assertEquals(this.b, this.lobby.challenger());
|
||||
assertTrue(this.lobby.canConfigure(this.b), "the new challenger may change the options");
|
||||
assertEquals(2, this.lobby.size(0));
|
||||
assertEquals(2, this.lobby.size(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handOverWhenAlone() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.lobby.leave(this.a);
|
||||
assertEquals("challenger b", this.events.getLast());
|
||||
assertTrue(this.lobby.canConfigure(this.b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void challengerMayChangeOptionsWhileOthersWait() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.configure(this.a, 2, 2, FightMode.OWN);
|
||||
this.lobby.enter(0, this.b);
|
||||
this.lobby.enter(1, this.c);
|
||||
|
||||
assertEquals(PadLobby.ConfigureResult.OWN_TOO_SMALL, this.lobby.configure(this.a, 1, 2, FightMode.OWN),
|
||||
"two already stand on the own pad");
|
||||
assertEquals(2, this.lobby.size(0));
|
||||
|
||||
this.events.clear();
|
||||
assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 2, 1, FightMode.CHALLENGER));
|
||||
assertEquals(List.of("kit b<-a", "kit c<-a", "countdown started"), this.events, "shrinking to the players there starts the countdown");
|
||||
assertTrue(this.lobby.isCountingDown());
|
||||
}
|
||||
|
||||
@Test
|
||||
void opponentSideCannotShrinkBelowPlayers() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.configure(this.a, 1, 3, FightMode.OWN);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.lobby.enter(1, this.c);
|
||||
assertEquals(PadLobby.ConfigureResult.OPPONENT_TOO_SMALL, this.lobby.configure(this.a, 1, 1, FightMode.OWN));
|
||||
assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 4, FightMode.OWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lastPlayerLeavingResetsLobby() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.leave(this.a);
|
||||
assertTrue(this.lobby.isIdle());
|
||||
assertNull(this.lobby.challenger());
|
||||
this.lobby.leave(this.b);
|
||||
assertEquals(List.of("challenger a first", "kit a<-a"), this.events);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullLobbyWaitsWhileChallengerChooses() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.setChoosing(this.a, true);
|
||||
this.lobby.enter(1, this.b);
|
||||
assertEquals("waiting", this.events.getLast());
|
||||
assertFalse(this.lobby.isCountingDown());
|
||||
assertTrue(this.lobby.isWaitingForChallenger());
|
||||
assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER), "the menu stays usable");
|
||||
|
||||
this.lobby.setChoosing(this.a, false);
|
||||
assertEquals("countdown started", this.events.getLast());
|
||||
assertFalse(this.lobby.isWaitingForChallenger());
|
||||
}
|
||||
|
||||
@Test
|
||||
void choosingTimesOut() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.setChoosing(this.a, true);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.run(60);
|
||||
assertFalse(this.lobby.isCountingDown());
|
||||
this.run(1);
|
||||
assertTrue(this.lobby.isCountingDown());
|
||||
this.lobby.setChoosing(this.a, false);
|
||||
assertEquals(1, this.events.stream().filter("countdown started"::equals).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyChallengerChoosingBlocks() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.setChoosing(this.b, true);
|
||||
this.lobby.enter(1, this.b);
|
||||
assertTrue(this.lobby.isCountingDown());
|
||||
}
|
||||
|
||||
@Test
|
||||
void opponentLeavingWhileWaitingIsQuiet() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.setChoosing(this.a, true);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.lobby.leave(this.b);
|
||||
assertFalse(this.events.contains("cancelled"));
|
||||
assertFalse(this.lobby.isWaitingForChallenger());
|
||||
}
|
||||
|
||||
@Test
|
||||
void newChallengerIsNotChoosing() {
|
||||
this.lobby.enter(0, this.a);
|
||||
this.lobby.setChoosing(this.a, true);
|
||||
this.lobby.enter(1, this.b);
|
||||
this.lobby.leave(this.a);
|
||||
this.lobby.enter(0, this.c);
|
||||
assertTrue(this.lobby.isCountingDown());
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RegionTest {
|
||||
@Test
|
||||
void cornersAreNormalized() {
|
||||
assertEquals(new Region(-2, 1, -5, 3, 4, 6), Region.of(3, 4, -5, -2, 1, 6));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsIsInclusiveAndFloorsNegativeCoordinates() {
|
||||
Region region = Region.of(-2, 0, -2, 1, 3, 1);
|
||||
assertTrue(region.contains(-2, 0, -2));
|
||||
assertTrue(region.contains(1, 3, 1));
|
||||
assertFalse(region.contains(2, 0, 0));
|
||||
assertTrue(region.contains(-1.5, 0.0, 1.99));
|
||||
assertFalse(region.contains(-2.01, 0.0, 0.0));
|
||||
assertFalse(region.contains(1.0, 4.0, 0.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void volumeAndChunks() {
|
||||
Region region = Region.of(-1, 0, 15, 16, 9, 32);
|
||||
assertEquals(18L * 10 * 18, region.volume());
|
||||
assertEquals(-1, region.minChunkX());
|
||||
assertEquals(1, region.maxChunkX());
|
||||
assertEquals(0, region.minChunkZ());
|
||||
assertEquals(2, region.maxChunkZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetCursorVisitsEveryBlockOnceInPortions() {
|
||||
Region region = Region.of(0, 0, 0, 4, 3, 2);
|
||||
ResetCursor cursor = new ResetCursor(region);
|
||||
Set<String> visited = new HashSet<>();
|
||||
int calls = 0;
|
||||
while(!cursor.isDone()) {
|
||||
int count = cursor.advance(7, (x, y, z) -> assertTrue(visited.add(x + "," + y + "," + z)));
|
||||
assertTrue(count <= 7);
|
||||
calls++;
|
||||
}
|
||||
assertEquals(region.volume(), visited.size());
|
||||
assertEquals((int) Math.ceil(region.volume() / 7.0), calls);
|
||||
assertEquals(0, cursor.advance(7, (x, y, z) -> fail()));
|
||||
}
|
||||
}
|
||||
+32
-47
@@ -1,47 +1,36 @@
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'java'
|
||||
id 'com.gradleup.shadow' version "9.6.1"
|
||||
}
|
||||
|
||||
group = 'eu.mhsl.craftattack'
|
||||
version = '1.0'
|
||||
allprojects {
|
||||
group = 'de.mhsl.craftattack'
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = "papermc-repo"
|
||||
url = "https://repo.papermc.io/repository/maven-public/"
|
||||
}
|
||||
maven {
|
||||
name = "sonatype"
|
||||
url = "https://oss.sonatype.org/content/groups/public/"
|
||||
}
|
||||
maven {
|
||||
url = uri("https://repo.opencollab.dev/main/")
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = "papermc-repo"
|
||||
url = "https://repo.papermc.io/repository/maven-public/"
|
||||
}
|
||||
maven {
|
||||
name = "sonatype"
|
||||
url = "https://oss.sonatype.org/content/groups/public/"
|
||||
}
|
||||
maven {
|
||||
url = uri("https://repo.opencollab.dev/main/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.2-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
implementation 'org.reflections:reflections:0.10.2'
|
||||
}
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'com.gradleup.shadow'
|
||||
|
||||
def targetJavaVersion = 21
|
||||
java {
|
||||
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||
sourceCompatibility = javaVersion
|
||||
targetCompatibility = javaVersion
|
||||
if (JavaVersion.current() < javaVersion) {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
|
||||
options.release = targetJavaVersion
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,18 +40,14 @@ configurations {
|
||||
|
||||
shadowJar {
|
||||
configurations = [project.configurations.shadowImplementation]
|
||||
archiveClassifier.set('')
|
||||
|
||||
relocate 'org.apache.httpcomponents', 'eu.mhsl.lib.shadow.httpclient'
|
||||
relocate 'com.sparkjava', 'eu.mhsl.lib.shadow.spark-core'
|
||||
|
||||
mergeServiceFiles()
|
||||
}
|
||||
|
||||
tasks.register('copyJarToServer', Exec) {
|
||||
dependsOn shadowJar
|
||||
mustRunAfter shadowJar
|
||||
|
||||
commandLine 'scp', 'build/libs/spawn-1.0-all.jar', 'root@10.20.6.1:/home/minecraft/server/plugins'
|
||||
}
|
||||
|
||||
tasks.register('copyJarToTestServer', Exec) {
|
||||
dependsOn shadowJar
|
||||
mustRunAfter shadowJar
|
||||
|
||||
commandLine 'cp', 'build/libs/spawn-1.0-all.jar', '/home/elias/Dokumente/mcTestServer/plugins/spawn-1.0-all.jar'
|
||||
if (file("local.gradle").exists()) {
|
||||
apply from: "local.gradle"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package eu.mhsl.craftattack.spawn.common.api;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.config.Configuration;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CraftAttackApi {
|
||||
private final static ConfigurationSection apiConfig = Objects.requireNonNull(Configuration.cfg.getConfigurationSection("api"));
|
||||
public final static String basePath = apiConfig.getString("baseurl");
|
||||
public final static String apiSecret = apiConfig.getString("secret");
|
||||
|
||||
public static URI getBaseUri() {
|
||||
Objects.requireNonNull(basePath);
|
||||
try {
|
||||
return new URI(basePath);
|
||||
} catch(URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void withAuthorizationSecret(URIBuilder builder) {
|
||||
builder.addParameter("secret", apiSecret);
|
||||
}
|
||||
|
||||
public static void withAuthorizationHeader(HttpRequest.Builder builder) {
|
||||
builder.header("Authorization", apiSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.mhsl.craftattack.spawn.common.api;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.config.Configuration;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Objects;
|
||||
|
||||
public class VaroApi {
|
||||
private final static ConfigurationSection apiConfig = Objects.requireNonNull(Configuration.cfg.getConfigurationSection("varoApi"));
|
||||
public final static String basePath = apiConfig.getString("endpoint");
|
||||
public final static String apiSecret = apiConfig.getString("auth");
|
||||
|
||||
public static URI getBaseUri() {
|
||||
Objects.requireNonNull(basePath);
|
||||
try {
|
||||
return new URI(basePath);
|
||||
} catch(URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void authorizationHeader(HttpRequest.Builder builder) {
|
||||
builder.header("Authorization", apiSecret);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package eu.mhsl.craftattack.spawn.common.api.repositories;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
|
||||
import eu.mhsl.craftattack.spawn.common.api.CraftAttackApi;
|
||||
import java.util.UUID;
|
||||
|
||||
public class CraftAttackReportRepository extends ReportRepository {
|
||||
public CraftAttackReportRepository() {
|
||||
super(CraftAttackApi.getBaseUri(), new RequestModifier(null, CraftAttackApi::withAuthorizationHeader));
|
||||
}
|
||||
|
||||
public ReqResp<PlayerReports> queryReports(UUID player) {
|
||||
return this.get(
|
||||
"users/%s/reports".formatted(player.toString()),
|
||||
PlayerReports.class
|
||||
);
|
||||
}
|
||||
|
||||
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
|
||||
return this.post(
|
||||
"reports",
|
||||
data,
|
||||
ReportUrl.class
|
||||
);
|
||||
}
|
||||
|
||||
public ReqResp<AllReports> queryAllReports() {
|
||||
return this.get("reports", AllReports.class);
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package eu.mhsl.craftattack.spawn.common.api.repositories;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.api.client.HttpRepository;
|
||||
import eu.mhsl.craftattack.spawn.core.api.client.RepositoryLoader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RepositoryLoader.Abstraction
|
||||
public abstract class ReportRepository extends HttpRepository {
|
||||
public ReportRepository(URI basePath, RequestModifier... baseRequestModifier) {
|
||||
super(basePath, baseRequestModifier);
|
||||
}
|
||||
|
||||
public record ReportCreationInfo(@NotNull UUID reporter, @Nullable UUID reported, String reason) {
|
||||
}
|
||||
|
||||
public record ReportUrl(@NotNull String url) {
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
open,
|
||||
closed,
|
||||
}
|
||||
|
||||
public record PlayerReports(
|
||||
List<Report> from_self,
|
||||
List<Report> to_self
|
||||
) {
|
||||
public record Report(
|
||||
@Nullable UUID reported,
|
||||
@NotNull String reason,
|
||||
@Nullable Long created,
|
||||
@Nullable Status status,
|
||||
@NotNull String url
|
||||
) { }
|
||||
}
|
||||
|
||||
public record AllReports(List<Report> reports) {
|
||||
public record Report(
|
||||
@NotNull UUID reporter,
|
||||
@Nullable UUID reported,
|
||||
@NotNull String reason,
|
||||
@Nullable Long created,
|
||||
@Nullable Status status,
|
||||
@NotNull String url
|
||||
) { }
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package eu.mhsl.craftattack.spawn.common.api.repositories;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.api.VaroApi;
|
||||
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaroReportRepository extends ReportRepository {
|
||||
public VaroReportRepository() {
|
||||
super(VaroApi.getBaseUri(), new RequestModifier(null, VaroApi::authorizationHeader));
|
||||
}
|
||||
|
||||
public ReqResp<PlayerReports> queryReports(UUID player) {
|
||||
throw new NotImplementedException("Report querying is not supported in Varo!");
|
||||
}
|
||||
|
||||
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
|
||||
return this.post(
|
||||
"report",
|
||||
data,
|
||||
ReportUrl.class
|
||||
);
|
||||
}
|
||||
|
||||
public record StrikeCreationInfo(
|
||||
@Nullable UUID reporter, // null for automatic creations
|
||||
@NotNull UUID reported,
|
||||
@NotNull String reason,
|
||||
@Nullable String body,
|
||||
@Nullable String notice,
|
||||
@Nullable String statement,
|
||||
int strike_reason_id // internal strike mapping
|
||||
) {
|
||||
}
|
||||
public ReqResp<Void> createStrike(StrikeCreationInfo data) {
|
||||
return this.put(
|
||||
"report",
|
||||
data,
|
||||
Void.class
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.gameplay.cordinateDisplay;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
|
||||
public class CoordinateChangedListener extends ApplianceListener<CoordinateDisplay> {
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
this.getAppliance().updateEnabled(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
if(!this.getAppliance().isEnabled(event.getPlayer())) return;
|
||||
boolean hasChangedOrientation = this.getAppliance().hasChangedDirection(event.getFrom(), event.getTo());
|
||||
if(!event.hasChangedBlock() && !hasChangedOrientation) return;
|
||||
this.getAppliance().sendCoordinates(event.getPlayer());
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.gameplay.cordinateDisplay;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.DataSizeConverter;
|
||||
import eu.mhsl.craftattack.spawn.core.util.world.WorldUtils;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class CoordinateDisplay extends Appliance {
|
||||
Map<Player, CoordinateDisplaySetting.CoordinateDisplayConfiguration> enabledPlayers = new WeakHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Settings.instance().declareSetting(CoordinateDisplaySetting.class);
|
||||
Settings.instance().addChangeListener(CoordinateDisplaySetting.class, this::updateEnabled);
|
||||
}
|
||||
|
||||
public void updateEnabled(Player player) {
|
||||
CoordinateDisplaySetting.CoordinateDisplayConfiguration configuration = Settings.instance().getSetting(
|
||||
player,
|
||||
Settings.Key.CoordinateDisplay,
|
||||
CoordinateDisplaySetting.CoordinateDisplayConfiguration.class
|
||||
);
|
||||
this.enabledPlayers.put(player, configuration);
|
||||
}
|
||||
|
||||
public boolean isEnabled(Player player) {
|
||||
return Optional.ofNullable(this.enabledPlayers.get(player))
|
||||
.map(CoordinateDisplaySetting.CoordinateDisplayConfiguration::anyEnabled)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
public void sendCoordinates(Player player) {
|
||||
CoordinateDisplaySetting.CoordinateDisplayConfiguration config = this.enabledPlayers.get(player);
|
||||
List<Component> components = new ArrayList<>();
|
||||
|
||||
if (config.coordinates()) {
|
||||
components.add(Component.text("\uD83C\uDF0E ", NamedTextColor.GOLD));
|
||||
components.add(Component.text(String.format(
|
||||
"%d %d %d",
|
||||
player.getLocation().getBlockX(),
|
||||
player.getLocation().getBlockY(),
|
||||
player.getLocation().getBlockZ()
|
||||
)));
|
||||
}
|
||||
|
||||
if (config.direction()) {
|
||||
if (!components.isEmpty()) {
|
||||
components.add(Component.text(" | ", NamedTextColor.GRAY));
|
||||
}
|
||||
components.add(Component.text("\uD83E\uDDED ", NamedTextColor.GOLD));
|
||||
components.add(Component.text(DataSizeConverter.getCardinalDirection(player.getLocation())));
|
||||
}
|
||||
|
||||
if (config.time()) {
|
||||
if (!components.isEmpty()) {
|
||||
components.add(Component.text(" | ", NamedTextColor.GRAY));
|
||||
}
|
||||
components.add(Component.text("⏱ ", NamedTextColor.GOLD));
|
||||
components.add(Component.text(WorldUtils.getGameTime(player.getWorld())));
|
||||
}
|
||||
|
||||
if (!components.isEmpty()) {
|
||||
Component actionBar = Component.empty();
|
||||
for (Component component : components) {
|
||||
actionBar = actionBar.append(component);
|
||||
}
|
||||
player.sendActionBar(actionBar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean hasChangedDirection(Location previous, Location next) {
|
||||
return !Objects.equals(
|
||||
DataSizeConverter.getCardinalDirection(previous),
|
||||
DataSizeConverter.getCardinalDirection(next)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new CoordinateChangedListener()
|
||||
);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.gameplay.cordinateDisplay;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.MultiBoolSetting;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class CoordinateDisplaySetting extends MultiBoolSetting<CoordinateDisplaySetting.CoordinateDisplayConfiguration> implements CategorizedSetting {
|
||||
public CoordinateDisplaySetting() {
|
||||
super(Settings.Key.CoordinateDisplay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SettingCategory category() {
|
||||
return SettingCategory.Gameplay;
|
||||
}
|
||||
|
||||
public record CoordinateDisplayConfiguration(
|
||||
@DisplayName("Koordinaten") boolean coordinates,
|
||||
@DisplayName("Richtung") boolean direction,
|
||||
@DisplayName("Zeit") boolean time
|
||||
) {
|
||||
public boolean anyEnabled() {
|
||||
return this.coordinates || this.direction || this.time;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String title() {
|
||||
return "Koordinatenanzeige";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String description() {
|
||||
return "Zeige deine aktuelle Position über der Hotbar an";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Material icon() {
|
||||
return Material.RECOVERY_COMPASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CoordinateDisplayConfiguration defaultValue() {
|
||||
return new CoordinateDisplayConfiguration(false, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> dataType() {
|
||||
return CoordinateDisplayConfiguration.class;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.internal.debug;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command.AppliancesCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command.UserInfoCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Debug extends Appliance {
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<ApplianceCommand<?>> commands() {
|
||||
return List.of(
|
||||
new UserInfoCommand(),
|
||||
new AppliancesCommand()
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.debug.command;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.Main;
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.appliances.debug.Debug;
|
||||
import eu.mhsl.craftattack.spawn.util.text.ComponentUtil;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.Debug;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentBuilder;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.debug.command;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.internal.debug.command;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.appliances.debug.Debug;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.internal.debug.Debug;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
@@ -50,13 +50,13 @@ public class UserInfoCommand extends ApplianceCommand<Debug> {
|
||||
.appendNewline()
|
||||
.append(
|
||||
Component
|
||||
.text("Erster Besuch: " + formatUnixTimestamp(player.getFirstPlayed()), NamedTextColor.GRAY)
|
||||
.text("Erster Besuch: " + this.formatUnixTimestamp(player.getFirstPlayed()), NamedTextColor.GRAY)
|
||||
.clickEvent(ClickEvent.copyToClipboard(String.valueOf(player.getFirstPlayed())))
|
||||
)
|
||||
.appendNewline()
|
||||
.append(
|
||||
Component
|
||||
.text("Letzter Besuch: " + formatUnixTimestamp(player.getLastSeen()), NamedTextColor.GRAY)
|
||||
.text("Letzter Besuch: " + this.formatUnixTimestamp(player.getLastSeen()), NamedTextColor.GRAY)
|
||||
.clickEvent(ClickEvent.copyToClipboard(String.valueOf(player.getLastSeen())))
|
||||
)
|
||||
.appendNewline()
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.titleClear;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.internal.titleClear;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.internal.titleClear;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
class TitleClearListener extends ApplianceListener<TitleClear> {
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
this.getAppliance().clearTitle(event.getPlayer());
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.adminMarker;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName.DisplayName;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class AdminMarker extends Appliance implements DisplayName.Colored {
|
||||
public final static String adminPermission = "adminmarker";
|
||||
|
||||
@Override
|
||||
public @Nullable TextColor getNameColor(Player player) {
|
||||
if(player.hasPermission(adminPermission))
|
||||
return TextColor.color(Color.AQUA.asRGB()); // TODO read permission from config
|
||||
return TextColor.color(Color.WHITE.asRGB());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
|
||||
apiBuilder.get("isAdmin", request -> {
|
||||
OfflinePlayer player = Bukkit.getOfflinePlayer(UUID.fromString(request.queryParams("player")));
|
||||
Main.logger().info(String.format("Adminstatus requested for %s, response: %s", player.getUniqueId(), player.isOp()));
|
||||
return player.isOp();
|
||||
});
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.networkChat.NetworkChat;
|
||||
import eu.mhsl.craftattack.spawn.core.util.server.Floodgate;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentBuilder;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class DisplayName extends Appliance {
|
||||
public interface Prefixed {
|
||||
@Nullable
|
||||
Component getNamePrefix(Player player);
|
||||
}
|
||||
|
||||
public interface Colored {
|
||||
@Nullable
|
||||
TextColor getNameColor(Player player);
|
||||
}
|
||||
|
||||
public void update(Player player) {
|
||||
List<Colored> coloring = Main.instance().getAppliances().stream()
|
||||
.filter(appliance -> appliance instanceof Colored)
|
||||
.map(appliance -> (Colored) appliance)
|
||||
.toList();
|
||||
|
||||
if(coloring.size() > 1) throw new IllegalStateException(
|
||||
"There are two or more appliances which provide coloring for player names. This is currently not supported!"
|
||||
);
|
||||
|
||||
TextColor playerColor = coloring.isEmpty()
|
||||
? NamedTextColor.WHITE
|
||||
: coloring.getFirst().getNameColor(player);
|
||||
|
||||
List<Prefixed> prefixes = Main.instance().getAppliances().stream()
|
||||
.filter(appliance -> appliance instanceof Prefixed)
|
||||
.map(appliance -> (Prefixed) appliance)
|
||||
.toList();
|
||||
|
||||
List<Component> prefixComponents = prefixes.stream()
|
||||
.map(prefixed -> prefixed.getNamePrefix(player))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
ComponentBuilder<TextComponent, TextComponent.Builder> playerName = Component.text();
|
||||
prefixComponents.forEach(prefix -> playerName
|
||||
.append(prefix)
|
||||
.append(ComponentUtil.clearedSpace())
|
||||
);
|
||||
|
||||
// Den Chat-Namen baut Velocity selbst (Farbe, Bedrock), es braucht von hier nur die Prefixe
|
||||
Main.instance().getAppliances().stream()
|
||||
.filter(NetworkChat.class::isInstance)
|
||||
.map(NetworkChat.class::cast)
|
||||
.findFirst()
|
||||
.ifPresent(networkChat -> networkChat.setPrefix(
|
||||
player,
|
||||
prefixComponents.isEmpty() ? null : Component.join(JoinConfiguration.separator(ComponentUtil.clearedSpace()), prefixComponents)
|
||||
));
|
||||
|
||||
if(Floodgate.isBedrock(player)) {
|
||||
playerName
|
||||
.append(
|
||||
Component.text("\uD83C\uDFAE", NamedTextColor.GRAY)
|
||||
.hoverEvent(HoverEvent.showText(Component.text(
|
||||
String.format("%s spielt die Minecraft: Bedrock Edition", player.getName())
|
||||
)))
|
||||
)
|
||||
.append(ComponentUtil.clearedSpace());
|
||||
}
|
||||
|
||||
playerName.append(Component.text(player.getName(), playerColor));
|
||||
|
||||
this.setGlobal(player, playerName.build());
|
||||
}
|
||||
|
||||
private void setGlobal(Player player, Component component) {
|
||||
try {
|
||||
player.customName(component);
|
||||
player.displayName(component);
|
||||
player.playerListName(component);
|
||||
} catch(Exception e) {
|
||||
Main.instance().getLogger().log(Level.SEVERE, e, e::getMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new DisplayNameUpdateListener());
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
class DisplayNameUpdateListener extends ApplianceListener<DisplayName> {
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
this.getAppliance().update(event.getPlayer());
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.DiscordCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.HelpCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.SpawnCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command.TeamspeakCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Help extends Appliance {
|
||||
public Help() {
|
||||
super("help");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<ApplianceCommand<?>> commands() {
|
||||
return List.of(
|
||||
new HelpCommand(),
|
||||
new SpawnCommand(),
|
||||
new TeamspeakCommand(),
|
||||
new DiscordCommand()
|
||||
);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class DiscordCommand extends ApplianceCommand<Help> {
|
||||
public DiscordCommand() {
|
||||
super("discord");
|
||||
}
|
||||
|
||||
private final static String discordLink = "https://discord.gg/TXxspGVanq";
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
|
||||
sender.sendMessage(
|
||||
Component.text("Offizieller Discord Server: ", NamedTextColor.GOLD)
|
||||
.append(Component.text(discordLink, NamedTextColor.AQUA))
|
||||
.clickEvent(ClickEvent.openUrl(discordLink))
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.help.command;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.appliances.help.Help;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
@@ -18,7 +18,8 @@ public class HelpCommand extends ApplianceCommand<Help> {
|
||||
sender.sendMessage(
|
||||
Component.text("Willkommen auf Craftattack!", NamedTextColor.GOLD)
|
||||
.appendNewline()
|
||||
.append(Component.text("Hier ist ein Hilfetext!", NamedTextColor.GRAY))
|
||||
.append(Component.text("Wenn du hilfe benötigst kannst du dich jederzeit an einen Admin wenden." +
|
||||
" Weitere Informationen zu Funktionen und Befehlen erhältst du zudem am Spawn.", NamedTextColor.GRAY))
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.help.command;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.appliances.help.Help;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
@@ -19,8 +19,8 @@ public class SpawnCommand extends ApplianceCommand<Help> {
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if(!getAppliance().localConfig().isString(spawnKey))
|
||||
if(!this.getAppliance().localConfig().isString(spawnKey))
|
||||
throw new ApplianceCommand.Error("Es wurde kein Spawnbereich hinterlegt!");
|
||||
sender.sendMessage(Component.text(Objects.requireNonNull(getAppliance().localConfig().getString(spawnKey)), NamedTextColor.GOLD));
|
||||
sender.sendMessage(Component.text(Objects.requireNonNull(this.getAppliance().localConfig().getString(spawnKey)), NamedTextColor.GOLD));
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.help.command;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.command;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.appliances.help.Help;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.help.Help;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
@@ -17,12 +17,12 @@ public class TeamspeakCommand extends ApplianceCommand<Help> {
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if(!getAppliance().localConfig().isString(teamspeakKey))
|
||||
if(!this.getAppliance().localConfig().isString(teamspeakKey))
|
||||
throw new ApplianceCommand.Error("Es wurde kein Teamspeak hinterlegt!");
|
||||
sender.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("Joine unserem Teamspeak: ", NamedTextColor.GOLD))
|
||||
.append(getTeamspeakIp(getAppliance().localConfig().getString(teamspeakKey)))
|
||||
.append(this.getTeamspeakIp(this.getAppliance().localConfig().getString(teamspeakKey)))
|
||||
);
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.util.Ticks;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public abstract class Bar {
|
||||
private BossBar bossBar;
|
||||
private final BukkitTask updateTask;
|
||||
public static String name;
|
||||
|
||||
public Bar() {
|
||||
long refreshRateInTicks = this.refresh().get(ChronoUnit.SECONDS) * Ticks.TICKS_PER_SECOND;
|
||||
this.updateTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
|
||||
Main.instance(),
|
||||
this::update,
|
||||
refreshRateInTicks,
|
||||
refreshRateInTicks
|
||||
);
|
||||
}
|
||||
|
||||
public BossBar getBossBar() {
|
||||
if(this.bossBar == null) this.bossBar = this.createBar();
|
||||
return this.bossBar;
|
||||
}
|
||||
|
||||
private BossBar createBar() {
|
||||
return BossBar.bossBar(
|
||||
this.title(),
|
||||
this.clampedProgress(),
|
||||
this.color(),
|
||||
this.overlay()
|
||||
);
|
||||
}
|
||||
|
||||
private void update() {
|
||||
if(this.bossBar == null) return;
|
||||
|
||||
this.beforeRefresh();
|
||||
this.bossBar.name(this.title());
|
||||
this.bossBar.progress(this.clampedProgress());
|
||||
this.bossBar.color(this.color());
|
||||
this.bossBar.overlay(this.overlay());
|
||||
}
|
||||
|
||||
public void stopUpdate() {
|
||||
this.updateTask.cancel();
|
||||
}
|
||||
|
||||
private float clampedProgress() {
|
||||
return Math.clamp(this.progress(), 0, 1);
|
||||
}
|
||||
|
||||
protected void beforeRefresh() {
|
||||
}
|
||||
|
||||
protected abstract Duration refresh();
|
||||
protected abstract String name();
|
||||
|
||||
protected abstract Component title();
|
||||
protected abstract float progress();
|
||||
protected abstract BossBar.Color color();
|
||||
protected abstract BossBar.Overlay overlay();
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.MultiBoolSetting;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class InfoBarSetting extends MultiBoolSetting<InfoBarSetting.InfoBarConfiguration> implements CategorizedSetting {
|
||||
public InfoBarSetting() {
|
||||
super(Settings.Key.InfoBars);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SettingCategory category() {
|
||||
return SettingCategory.Misc;
|
||||
}
|
||||
|
||||
public record InfoBarConfiguration(
|
||||
@DisplayName("Millisekunden pro Tick") boolean mspt,
|
||||
@DisplayName("Spieler online") boolean playerCounter,
|
||||
@DisplayName("Ticks pro Sekunde") boolean tps,
|
||||
@DisplayName("TPS, MSPT & Spieler") boolean tpsMsptPlayerCounter
|
||||
) {}
|
||||
|
||||
@Override
|
||||
protected String title() {
|
||||
return "Informationsleisten";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String description() {
|
||||
return "Wähle anzuzeigende Informationsleisten aus";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Material icon() {
|
||||
return Material.COMMAND_BLOCK;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InfoBarConfiguration defaultValue() {
|
||||
return new InfoBarConfiguration(false, false, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> dataType() {
|
||||
return InfoBarConfiguration.class;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.TpsMsptPlayerCounterBar;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.MsptBar;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.PlayerCounterBar;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars.TpsBar;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class InfoBars extends Appliance {
|
||||
private final List<Bar> infoBars = List.of(
|
||||
new TpsBar(),
|
||||
new MsptBar(),
|
||||
new PlayerCounterBar(),
|
||||
new TpsMsptPlayerCounterBar()
|
||||
);
|
||||
|
||||
public void showAllEnabled(Player player) {
|
||||
InfoBarSetting.InfoBarConfiguration config = Settings.instance().getSetting(player, Settings.Key.InfoBars, InfoBarSetting.InfoBarConfiguration.class);
|
||||
this.setVisible(player, MsptBar.name, config.mspt());
|
||||
this.setVisible(player, PlayerCounterBar.name, config.playerCounter());
|
||||
this.setVisible(player, TpsBar.name, config.tps());
|
||||
this.setVisible(player, TpsMsptPlayerCounterBar.name, config.tpsMsptPlayerCounter());
|
||||
}
|
||||
|
||||
public void setVisible(Player player, String bar, boolean visible) {
|
||||
this.validateBarName(bar);
|
||||
if(visible) {
|
||||
player.showBossBar(this.getBarByName(bar).getBossBar());
|
||||
} else {
|
||||
player.hideBossBar(this.getBarByName(bar).getBossBar());
|
||||
}
|
||||
}
|
||||
|
||||
private Bar getBarByName(String name) {
|
||||
return this.infoBars.stream()
|
||||
.filter(bar -> bar.name().equalsIgnoreCase(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void validateBarName(String name) {
|
||||
if(this.getBarByName(name) == null)
|
||||
throw new ApplianceCommand.Error(String.format("Ungültiger infobar name '%s'", name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Settings.instance().declareSetting(InfoBarSetting.class);
|
||||
Settings.instance().addChangeListener(InfoBarSetting.class, this::showAllEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
this.infoBars.forEach(Bar::stopUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new ShowPreviousBarsListener());
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
class ShowPreviousBarsListener extends ApplianceListener<InfoBars> {
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
this.getAppliance().showAllEnabled(event.getPlayer());
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
|
||||
import eu.mhsl.craftattack.spawn.core.util.statistics.ServerMonitor;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class MsptBar extends Bar {
|
||||
public static String name = "msptd";
|
||||
|
||||
@Override
|
||||
protected Duration refresh() {
|
||||
return Duration.ofSeconds(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component title() {
|
||||
return Component.text()
|
||||
.append(Component.text("M"))
|
||||
.append(Component.text("illi", NamedTextColor.GRAY))
|
||||
.append(Component.text("s"))
|
||||
.append(Component.text("ekunden ", NamedTextColor.GRAY))
|
||||
.append(Component.text("p"))
|
||||
.append(Component.text("ro ", NamedTextColor.GRAY))
|
||||
.append(Component.text("T"))
|
||||
.append(Component.text("ick", NamedTextColor.GRAY))
|
||||
.append(Component.text(": "))
|
||||
.append(Component.text(String.format("%.2f", this.currentMSPT()), ColorUtil.msptColor(this.currentMSPT())))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float progress() {
|
||||
return this.currentMSPT() / 50f;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Color color() {
|
||||
return this.currentMSPT() <= 50 ? BossBar.Color.GREEN : BossBar.Color.RED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Overlay overlay() {
|
||||
return BossBar.Overlay.PROGRESS;
|
||||
}
|
||||
|
||||
private float currentMSPT() {
|
||||
return ServerMonitor.getServerMSPT();
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.playerlimit.PlayerLimit;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class PlayerCounterBar extends Bar {
|
||||
public static String name = "playerCounter";
|
||||
|
||||
@Override
|
||||
protected Duration refresh() {
|
||||
return Duration.ofSeconds(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component title() {
|
||||
TextColor color = ColorUtil.mapGreenToRed(this.getCurrentPlayerCount(), 0, this.getMaxPlayerCount(), true);
|
||||
return Component.text()
|
||||
.append(Component.text("Spieler online: "))
|
||||
.append(Component.text(this.getCurrentPlayerCount(), color))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float progress() {
|
||||
return (float) this.getCurrentPlayerCount() / this.getMaxPlayerCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Color color() {
|
||||
int freeSlots = this.getMaxPlayerCount() - this.getCurrentPlayerCount();
|
||||
return freeSlots <= 0
|
||||
? BossBar.Color.RED
|
||||
: freeSlots < 5 ? BossBar.Color.YELLOW : BossBar.Color.GREEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Overlay overlay() {
|
||||
return BossBar.Overlay.PROGRESS;
|
||||
}
|
||||
|
||||
private int getCurrentPlayerCount() {
|
||||
return Bukkit.getOnlinePlayers().size();
|
||||
}
|
||||
|
||||
private int getMaxPlayerCount() {
|
||||
return Main.instance().getAppliance(PlayerLimit.class).getLimit();
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class TpsBar extends Bar {
|
||||
public static String name = "tps";
|
||||
|
||||
@Override
|
||||
protected Duration refresh() {
|
||||
return Duration.ofSeconds(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component title() {
|
||||
return Component.text()
|
||||
.append(Component.text("T"))
|
||||
.append(Component.text("icks ", NamedTextColor.GRAY))
|
||||
.append(Component.text("p"))
|
||||
.append(Component.text("ro ", NamedTextColor.GRAY))
|
||||
.append(Component.text("S"))
|
||||
.append(Component.text("ekunde", NamedTextColor.GRAY))
|
||||
.append(Component.text(": "))
|
||||
.append(Component.text(String.format("%.2f", this.currentTps()), ColorUtil.tpsColor(this.currentTps())))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float progress() {
|
||||
return this.currentTps() / 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Color color() {
|
||||
return this.currentTps() >= 18
|
||||
? BossBar.Color.GREEN
|
||||
: this.currentTps() >= 15 ? BossBar.Color.YELLOW : BossBar.Color.RED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Overlay overlay() {
|
||||
return BossBar.Overlay.NOTCHED_20;
|
||||
}
|
||||
|
||||
private float currentTps() {
|
||||
return (float) Bukkit.getTPS()[0];
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.bars;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.infoBars.Bar;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.playerlimit.PlayerLimit;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.util.statistics.ServerMonitor;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ColorUtil;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class TpsMsptPlayerCounterBar extends Bar {
|
||||
public static String name = "serverStats";
|
||||
|
||||
@Override
|
||||
protected Duration refresh() {
|
||||
return Duration.ofSeconds(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component title() {
|
||||
float tps = this.currentTps();
|
||||
float mspt = this.currentMspt();
|
||||
int players = this.currentPlayerCount();
|
||||
|
||||
Component separator = Component.text(" | ", NamedTextColor.DARK_GRAY);
|
||||
TextColor color = ColorUtil.mapGreenToRed(players, 0, this.getMaxPlayerCount(), true);
|
||||
|
||||
return Component.text()
|
||||
.append(Component.text("TPS: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(String.format("%.2f", tps), ColorUtil.tpsColor(tps)))
|
||||
.append(separator)
|
||||
.append(Component.text("MSPT: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(String.format("%.2f", mspt), ColorUtil.msptColor(mspt)))
|
||||
.append(separator)
|
||||
.append(Component.text("Spieler: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(players, color))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float progress() {
|
||||
return Math.clamp(this.currentTps() / 20f, 0f, 1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Color color() {
|
||||
int severity = Math.max(
|
||||
this.tpsSeverity(),
|
||||
Math.max(this.msptSeverity(), this.playerSeverity())
|
||||
);
|
||||
|
||||
return switch (severity) {
|
||||
case 2 -> BossBar.Color.RED;
|
||||
case 1 -> BossBar.Color.YELLOW;
|
||||
default -> BossBar.Color.GREEN;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BossBar.Overlay overlay() {
|
||||
return BossBar.Overlay.PROGRESS;
|
||||
}
|
||||
|
||||
private int tpsSeverity() {
|
||||
float tps = this.currentTps();
|
||||
return tps >= 18 ? 0 : tps >= 15 ? 1 : 2;
|
||||
}
|
||||
|
||||
private int msptSeverity() {
|
||||
return this.currentMspt() <= 50 ? 0 : 2;
|
||||
}
|
||||
|
||||
private int playerSeverity() {
|
||||
int freeSlots = this.getMaxPlayerCount() - this.currentPlayerCount();
|
||||
return freeSlots <= 0 ? 2 : freeSlots < 5 ? 1 : 0;
|
||||
}
|
||||
|
||||
private float currentTps() {
|
||||
return (float) Bukkit.getTPS()[0];
|
||||
}
|
||||
|
||||
private float currentMspt() {
|
||||
return ServerMonitor.getServerMSPT();
|
||||
}
|
||||
|
||||
private int currentPlayerCount() {
|
||||
return Bukkit.getOnlinePlayers().size();
|
||||
}
|
||||
|
||||
private int getMaxPlayerCount() {
|
||||
return Main.instance().getAppliance(PlayerLimit.class).getLimit();
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -1,9 +1,9 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.chatMention;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.networkChat;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.MultiBoolSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.MultiBoolSetting;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class ChatMentionSetting extends MultiBoolSetting<ChatMentionSetting.ChatMentionConfig> implements CategorizedSetting {
|
||||
@@ -15,7 +15,8 @@ public class ChatMentionSetting extends MultiBoolSetting<ChatMentionSetting.Chat
|
||||
public record ChatMentionConfig(
|
||||
@DisplayName("Spielernamen hervorheben") boolean applyMentions,
|
||||
@DisplayName("Benachrichtigungston") boolean notifyOnMention
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public ChatMentionSetting() {
|
||||
super(Settings.Key.ChatMentions);
|
||||
@@ -28,7 +29,7 @@ public class ChatMentionSetting extends MultiBoolSetting<ChatMentionSetting.Chat
|
||||
|
||||
@Override
|
||||
protected String description() {
|
||||
return "Erwähnungen werden automatisch im Chat angewandt und der Empfänger erhält einen Signalton";
|
||||
return "Wirst du im Chat erwähnt, wird dein Name hervorgehoben und du hörst einen Signalton";
|
||||
}
|
||||
|
||||
@Override
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.networkChat;
|
||||
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Chat läuft über Velocity (Plugin chat-bridge): Der öffentliche Chat, /msg, /r, /adminchat, /mute und
|
||||
* Join/Leave-Meldungen gelten im ganzen Netzwerk. Hier wird der Chat nur verworfen, Velocity bekommt
|
||||
* die Rang-Prefixe für den Namen, und die Chat-Einstellungen werden mit Velocity abgeglichen.
|
||||
* <p>
|
||||
* Kanal {@value #channel}, Nachrichten (UTF-Typ, dann Daten):
|
||||
* <ul>
|
||||
* <li>→ Proxy {@code prefix}: Adventure-JSON oder leer</li>
|
||||
* <li>→ Proxy {@code settings-hello} beim Join, {@code settings-set} nach einer Änderung: Join/Leave,
|
||||
* Erwähnungen hervorheben, Erwähnungston (je boolean). Velocity speichert sie; kennt es schon
|
||||
* Werte, antwortet es auf das Hello mit {@code settings}.</li>
|
||||
* <li>← Proxy {@code settings}: gespeicherte Werte, werden hier übernommen</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class NetworkChat extends Appliance {
|
||||
public static final String channel = "craftattack:chat";
|
||||
|
||||
private final Map<UUID, Component> prefixes = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getMessenger().registerOutgoingPluginChannel(Main.instance(), channel);
|
||||
Bukkit.getMessenger().registerIncomingPluginChannel(Main.instance(), channel, (channel, player, message) -> this.receive(player, message));
|
||||
|
||||
Settings.instance().declareSetting(ShowJoinAndLeaveMessagesSetting.class);
|
||||
Settings.instance().declareSetting(ChatMentionSetting.class);
|
||||
Settings.instance().addChangeListener(ShowJoinAndLeaveMessagesSetting.class, player -> this.sendSettings(player, "settings-set"));
|
||||
Settings.instance().addChangeListener(ChatMentionSetting.class, player -> this.sendSettings(player, "settings-set"));
|
||||
}
|
||||
|
||||
public void setPrefix(Player player, @Nullable Component prefix) {
|
||||
if(prefix == null) this.prefixes.remove(player.getUniqueId());
|
||||
else this.prefixes.put(player.getUniqueId(), prefix);
|
||||
this.sendPrefix(player);
|
||||
}
|
||||
|
||||
void sendPrefix(Player player) {
|
||||
Component prefix = this.prefixes.get(player.getUniqueId());
|
||||
ByteArrayDataOutput output = ByteStreams.newDataOutput();
|
||||
output.writeUTF("prefix");
|
||||
output.writeUTF(prefix == null ? "" : GsonComponentSerializer.gson().serialize(prefix));
|
||||
this.send(player, output);
|
||||
}
|
||||
|
||||
void sendSettings(Player player, String type) {
|
||||
ChatMentionSetting.ChatMentionConfig mentions = Settings.instance()
|
||||
.getSetting(player, Settings.Key.ChatMentions, ChatMentionSetting.ChatMentionConfig.class);
|
||||
ByteArrayDataOutput output = ByteStreams.newDataOutput();
|
||||
output.writeUTF(type);
|
||||
output.writeBoolean(Settings.instance().getSetting(player, Settings.Key.ShowJoinAndLeaveMessages, Boolean.class));
|
||||
output.writeBoolean(mentions.applyMentions());
|
||||
output.writeBoolean(mentions.notifyOnMention());
|
||||
this.send(player, output);
|
||||
}
|
||||
|
||||
private void receive(Player player, byte[] message) {
|
||||
ByteArrayDataInput input = ByteStreams.newDataInput(message);
|
||||
if(!input.readUTF().equals("settings")) return;
|
||||
boolean joinAndLeave = input.readBoolean();
|
||||
boolean applyMentions = input.readBoolean();
|
||||
boolean notifyOnMention = input.readBoolean();
|
||||
Settings.instance().setSetting(player, Settings.Key.ShowJoinAndLeaveMessages, joinAndLeave);
|
||||
Settings.instance().setSetting(player, Settings.Key.ChatMentions, new ChatMentionSetting.ChatMentionConfig(applyMentions, notifyOnMention));
|
||||
}
|
||||
|
||||
private void send(Player player, ByteArrayDataOutput output) {
|
||||
// Velocity meldet den Kanal erst kurz nach dem Join an, dann folgt PlayerRegisterChannelEvent
|
||||
if(!player.getListeningPluginChannels().contains(channel)) return;
|
||||
player.sendPluginMessage(Main.instance(), channel, output.toByteArray());
|
||||
}
|
||||
|
||||
void forget(Player player) {
|
||||
this.prefixes.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new NetworkChatListener());
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.networkChat;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import io.papermc.paper.event.player.AsyncChatEvent;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerRegisterChannelEvent;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
class NetworkChatListener extends ApplianceListener<NetworkChat> {
|
||||
// Vanilla-Befehle, die am Proxy vorbei in den lokalen Chat schreiben; /msg, /tell, /w fängt Velocity ab
|
||||
private static final Set<String> localChatCommands = Set.of(
|
||||
"me", "minecraft:me", "teammsg", "tm", "minecraft:teammsg", "minecraft:tm",
|
||||
"minecraft:msg", "minecraft:tell", "minecraft:w"
|
||||
);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onChat(AsyncChatEvent event) {
|
||||
// Velocity hat die Nachricht schon an alle verteilt
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onCommand(PlayerCommandPreprocessEvent event) {
|
||||
String label = event.getMessage().substring(1).split(" ", 2)[0].toLowerCase(Locale.ROOT);
|
||||
if(!localChatCommands.contains(label)) return;
|
||||
event.setCancelled(true);
|
||||
event.getPlayer().sendMessage(Component.text("Dieser Befehl ist hier nicht verfügbar.", NamedTextColor.RED));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRegisterChannel(PlayerRegisterChannelEvent event) {
|
||||
if(!event.getChannel().equals(NetworkChat.channel)) return;
|
||||
this.getAppliance().sendPrefix(event.getPlayer());
|
||||
this.getAppliance().sendSettings(event.getPlayer(), "settings-hello");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
// Join- und Leave-Meldungen schreibt Velocity, beim Serverwechsel gar keine
|
||||
event.joinMessage(null);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
event.quitMessage(null);
|
||||
this.getAppliance().forget(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
event.deathMessage(
|
||||
Component
|
||||
.text("☠ ")
|
||||
.append(
|
||||
Optional
|
||||
.ofNullable(event.deathMessage())
|
||||
.orElse(Component.text(event.getPlayer().getName()))
|
||||
)
|
||||
.color(TextColor.color(Color.SILVER.asRGB()))
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.chatMessages;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.networkChat;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.BoolSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class ShowJoinAndLeaveMessagesSetting extends BoolSetting implements CategorizedSetting {
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.api.repositories.ReportRepository;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners.ReportCreatedListener;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners.ReportJoinListener;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.api.client.ReqResp;
|
||||
import eu.mhsl.craftattack.spawn.common.api.repositories.CraftAttackReportRepository;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentBuilder;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class Report extends Appliance {
|
||||
public static Component helpText() {
|
||||
return Component.text()
|
||||
.appendNewline()
|
||||
.append(Component.text(" Um einen Spieler zu melden, verwende ", NamedTextColor.GRAY)).appendNewline()
|
||||
.append(Component.text("/report", NamedTextColor.GOLD)).appendNewline()
|
||||
.append(Component.text("oder", NamedTextColor.GRAY)).appendNewline()
|
||||
.append(Component.text("/report <spieler> [grund]", NamedTextColor.GOLD)).appendNewline()
|
||||
.build();
|
||||
}
|
||||
|
||||
public Report() {
|
||||
super("report");
|
||||
}
|
||||
|
||||
public void reportToUnknown(@NotNull Player issuer) {
|
||||
CraftAttackReportRepository.ReportCreationInfo request = new CraftAttackReportRepository.ReportCreationInfo(issuer.getUniqueId(), null, "");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(
|
||||
Main.instance(),
|
||||
() -> this.createReport(issuer, request)
|
||||
);
|
||||
}
|
||||
|
||||
public void reportToKnown(@NotNull Player issuer, @NotNull String targetUsername, @Nullable String reason) {
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(targetUsername);
|
||||
if(issuer.getUniqueId().equals(offlinePlayer.getUniqueId())) {
|
||||
issuer.sendMessage(Component.text("Du kannst dich nicht selbst reporten.", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
ReportRepository.ReportCreationInfo request = new ReportRepository.ReportCreationInfo(
|
||||
issuer.getUniqueId(),
|
||||
offlinePlayer.getUniqueId(),
|
||||
Optional.ofNullable(reason).orElse("")
|
||||
);
|
||||
Bukkit.getScheduler().runTaskAsynchronously(
|
||||
Main.instance(),
|
||||
() -> this.createReport(issuer, request)
|
||||
);
|
||||
}
|
||||
|
||||
private void createReport(Player issuer, ReportRepository.ReportCreationInfo reportRequest) {
|
||||
ReqResp<ReportRepository.ReportUrl> createdReport = this.queryRepository(CraftAttackReportRepository.class) // TODO: Besser machen!!
|
||||
.createReport(reportRequest);
|
||||
|
||||
switch(createdReport.status()) {
|
||||
case 200: // varo-endpoint specific
|
||||
case 201:
|
||||
issuer.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("\\/".repeat(20), NamedTextColor.DARK_GRAY))
|
||||
.appendNewline()
|
||||
.append(Component.text("⚠ Der Report muss über den folgenden Link fertiggestellt werden:", NamedTextColor.GOLD))
|
||||
.appendNewline()
|
||||
.appendNewline()
|
||||
.append(
|
||||
Component
|
||||
.text(createdReport.data().url(), NamedTextColor.GRAY)
|
||||
.clickEvent(ClickEvent.openUrl(createdReport.data().url()))
|
||||
)
|
||||
.appendNewline()
|
||||
.appendNewline()
|
||||
.append(Component.text("Ohne das Fertigstellen des Reports wird dieser nicht bearbeitet!", NamedTextColor.DARK_RED))
|
||||
.appendNewline()
|
||||
.append(Component.text("/\\".repeat(20), NamedTextColor.DARK_GRAY))
|
||||
);
|
||||
break;
|
||||
|
||||
case 400:
|
||||
issuer.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("Der angegebene Nutzer ist in unserem System nicht bekannt.", NamedTextColor.RED))
|
||||
.appendNewline()
|
||||
.append(Component.text("Bist du sicher, dass du den Namen richtig geschrieben hast?", NamedTextColor.RED))
|
||||
.appendNewline()
|
||||
.append(Component.text("Du kannst dich alternativ jederzeit bei einem Admin melden.", NamedTextColor.GRAY))
|
||||
);
|
||||
break;
|
||||
|
||||
case 401:
|
||||
default:
|
||||
Main.logger().warning("Failed to request Report: " + createdReport.status());
|
||||
issuer.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("Interner Serverfehler beim anlegen des Reports.", NamedTextColor.RED))
|
||||
.appendNewline()
|
||||
.append(Component.text("Bitte melde dich bei einem Admin!", NamedTextColor.RED))
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void queryReports(Player issuer) {
|
||||
ReqResp<ReportRepository.PlayerReports> userReports = this.queryRepository(CraftAttackReportRepository.class) // TODO: Besser machen!!
|
||||
.queryReports(issuer.getUniqueId());
|
||||
|
||||
if(userReports.status() != 200) {
|
||||
Main.logger().warning("Failed to request Reports: " + userReports.status());
|
||||
issuer.sendMessage(
|
||||
Component.text()
|
||||
.append(Component.text("Interner Serverfehler beim abfragen der Reports.", NamedTextColor.RED))
|
||||
.appendNewline()
|
||||
.append(Component.text("Bitte melde dich bei einem Admin!", NamedTextColor.RED))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Function<List<ReportRepository.PlayerReports.Report>, List<ReportRepository.PlayerReports.Report>> filterClosed = reports -> reports.stream()
|
||||
.filter(report -> Objects.equals(report.status(), ReportRepository.Status.closed))
|
||||
.toList();
|
||||
|
||||
List<ReportRepository.PlayerReports.Report> reportsToOthers = filterClosed.apply(userReports.data().from_self()).reversed();
|
||||
List<ReportRepository.PlayerReports.Report> reportsToSelf = filterClosed.apply(userReports.data().to_self()).reversed();
|
||||
|
||||
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
|
||||
.append(Component.text(
|
||||
!reportsToSelf.isEmpty()
|
||||
? "Du wurdest insgesamt %d mal von anderen Spielern gemeldet.".formatted(reportsToSelf.size())
|
||||
: "Du wurdest von keinem anderen Spieler gemeldet.",
|
||||
NamedTextColor.GOLD)
|
||||
);
|
||||
|
||||
component.appendNewline();
|
||||
|
||||
component.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD));
|
||||
reportsToOthers.forEach(report -> {
|
||||
Component button = Component.text("[\uD83D\uDC41/\uD83D\uDD8A]")
|
||||
.clickEvent(ClickEvent.openUrl(report.url()))
|
||||
.hoverEvent(HoverEvent.showText(ComponentUtil.clickLink(report.url())));
|
||||
|
||||
Component reportedDisplayName = report.reported() != null
|
||||
? Component.text(Optional.ofNullable(Bukkit.getOfflinePlayer(report.reported()).getName()).orElse(report.reported().toString()), NamedTextColor.WHITE)
|
||||
: Component.text("Unbekannt", NamedTextColor.YELLOW);
|
||||
|
||||
component
|
||||
.appendNewline()
|
||||
.append(Component.text(" \u27A1 ", NamedTextColor.GRAY))
|
||||
.append(button)
|
||||
.append(Component.text(" du gegen ", NamedTextColor.GRAY))
|
||||
.append(reportedDisplayName)
|
||||
.append(Component.text(String.format(": %s", report.reason()), NamedTextColor.GRAY));
|
||||
});
|
||||
|
||||
if(reportsToOthers.isEmpty()) {
|
||||
component
|
||||
.appendNewline()
|
||||
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
|
||||
.appendNewline()
|
||||
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
issuer.sendMessage(component.build());
|
||||
}
|
||||
|
||||
public void sendReportsInfo(Player player) {
|
||||
ReqResp<ReportRepository.AllReports> allReportsResponse = this.queryRepository(CraftAttackReportRepository.class).queryAllReports();
|
||||
|
||||
if(allReportsResponse.status() != 200) {
|
||||
Main.logger().warning("Failed to request Reports: " + allReportsResponse.status());
|
||||
return;
|
||||
}
|
||||
|
||||
List<ReportRepository.AllReports.Report> allOpenReports = allReportsResponse.data().reports().stream()
|
||||
.filter(report -> report.status() == null && report.created() != null)
|
||||
.toList();
|
||||
|
||||
if(allOpenReports.isEmpty()) return;
|
||||
|
||||
player.sendMessage(
|
||||
Component.text("Hey, es gibt noch ", NamedTextColor.GOLD)
|
||||
.append(Component.text(allOpenReports.size(), NamedTextColor.RED))
|
||||
.append(Component.text(" unbearbeitete Reports!", NamedTextColor.GOLD))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new ReportCreatedListener(),
|
||||
new ReportJoinListener()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<ApplianceCommand<?>> commands() {
|
||||
return List.of(
|
||||
new ReportCommand(),
|
||||
new ReportsCommand()
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
-16
@@ -1,8 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.report;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.Command;
|
||||
@@ -14,34 +13,29 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ReportCommand extends ApplianceCommand.PlayerChecked<Report> {
|
||||
class ReportCommand extends ApplianceCommand.PlayerChecked<Report> {
|
||||
public ReportCommand() {
|
||||
super("report");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
sender.sendMessage(
|
||||
Component.newline()
|
||||
.append(Component.text("Reportanfrage wird erstellt...", NamedTextColor.GREEN))
|
||||
.appendNewline()
|
||||
.append(Component.text("Bitte warte einen Augenblick", NamedTextColor.GRAY))
|
||||
.appendNewline()
|
||||
);
|
||||
sender.sendMessage(ComponentUtil.pleaseWait());
|
||||
|
||||
if(args.length == 0) {
|
||||
getAppliance().reportToUnknown(getPlayer());
|
||||
this.getAppliance().reportToUnknown(this.getPlayer());
|
||||
}
|
||||
|
||||
if(args.length == 1) {
|
||||
getAppliance().reportToKnown(getPlayer(), args[0], null);
|
||||
this.getAppliance().reportToKnown(this.getPlayer(), args[0], null);
|
||||
}
|
||||
|
||||
if(args.length > 1) {
|
||||
getAppliance().reportToKnown(getPlayer(), args[0], Arrays.stream(args).skip(1).collect(Collectors.joining(" ")));
|
||||
this.getAppliance().reportToKnown(this.getPlayer(), args[0], Arrays.stream(args).skip(1).collect(Collectors.joining(" ")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +47,7 @@ public class ReportCommand extends ApplianceCommand.PlayerChecked<Report> {
|
||||
response = Stream.concat(
|
||||
Bukkit.getOnlinePlayers().stream().map(Player::getName),
|
||||
Arrays.stream(Bukkit.getOfflinePlayers()).map(OfflinePlayer::getName)
|
||||
).toList();
|
||||
).filter(Objects::nonNull).distinct().toList();
|
||||
}
|
||||
|
||||
if(args.length == 2) {
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class ReportsCommand extends ApplianceCommand.PlayerChecked<Report> {
|
||||
public ReportsCommand() {
|
||||
super("reports");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
sender.sendMessage(ComponentUtil.pleaseWait());
|
||||
Bukkit.getScheduler().runTaskAsynchronously(
|
||||
Main.instance(),
|
||||
() -> this.getAppliance().queryReports(this.getPlayer())
|
||||
);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.Report;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import eu.mhsl.craftattack.spawn.core.event.ReportCreatedEvent;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.event.EventHandler;
|
||||
|
||||
public class ReportCreatedListener extends ApplianceListener<Report> {
|
||||
@EventHandler
|
||||
public void onReport(ReportCreatedEvent event) {
|
||||
OfflinePlayer reporter = Bukkit.getOfflinePlayer(event.getReport().reporter());
|
||||
OfflinePlayer reported = Bukkit.getOfflinePlayer(event.getReport().reported());
|
||||
|
||||
Component message = Component.text(
|
||||
"\uD83D\uDD14 Neuer Report von %s gegen %s: %s".formatted(reporter.getName(), reported.getName(), event.getReport().reason()),
|
||||
NamedTextColor.YELLOW
|
||||
);
|
||||
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> player.hasPermission("admin"))
|
||||
.forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.listeners;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.Report;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
public class ReportJoinListener extends ApplianceListener<Report> {
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if(!event.getPlayer().hasPermission("admin")) return;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(
|
||||
Main.instance(),
|
||||
() -> this.getAppliance().sendReportsInfo(event.getPlayer())
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
|
||||
|
||||
public interface CategorizedSetting {
|
||||
SettingCategory category();
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
|
||||
|
||||
public enum SettingCategory {
|
||||
Gameplay,
|
||||
Visuals,
|
||||
Misc,
|
||||
}
|
||||
+69
-27
@@ -1,10 +1,12 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.Setting;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.listeners.OpenSettingsShortcutListener;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.listeners.SettingsInventoryListener;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.Setting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners.OpenSettingsShortcutListener;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners.SettingsInventoryListener;
|
||||
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -15,6 +17,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Settings extends Appliance {
|
||||
@@ -32,11 +35,16 @@ public class Settings extends Appliance {
|
||||
ChatMentions,
|
||||
DoubleDoors,
|
||||
KnockDoors,
|
||||
BorderWarning,
|
||||
LocatorBar,
|
||||
InfoBars,
|
||||
CoordinateDisplay,
|
||||
Bloodmoon
|
||||
}
|
||||
|
||||
public static Settings instance() {
|
||||
if(settingsInstance != null) return settingsInstance;
|
||||
Settings.settingsInstance = queryAppliance(Settings.class);
|
||||
Settings.settingsInstance = Main.instance().getAppliance(Settings.class);
|
||||
return settingsInstance;
|
||||
}
|
||||
|
||||
@@ -55,22 +63,32 @@ public class Settings extends Appliance {
|
||||
|
||||
private final WeakHashMap<Player, OpenSettingsInventory> openSettingsInventories = new WeakHashMap<>();
|
||||
private final WeakHashMap<Player, List<Setting<?>>> settingsCache = new WeakHashMap<>();
|
||||
protected final Map<Class<? extends Setting<?>>, Consumer<Player>> changeListeners = new WeakHashMap<>();
|
||||
|
||||
public <TDataType extends Setting<?>> void addChangeListener(Class<TDataType> setting, Consumer<Player> listener) {
|
||||
this.changeListeners.merge(setting, listener, Consumer::andThen);
|
||||
}
|
||||
|
||||
public <TDataType extends Setting<?>> void invokeChangeListener(Player player, Class<TDataType> setting) {
|
||||
Optional.ofNullable(this.changeListeners.get(setting))
|
||||
.ifPresent(listener -> listener.accept(player));
|
||||
}
|
||||
|
||||
private List<Setting<?>> getSettings(Player player) {
|
||||
if(settingsCache.containsKey(player)) return settingsCache.get(player);
|
||||
if(this.settingsCache.containsKey(player)) return this.settingsCache.get(player);
|
||||
|
||||
List<Setting<?>> settings = this.declaredSettings.stream()
|
||||
.map(clazz -> {
|
||||
try {
|
||||
return clazz.getDeclaredConstructor();
|
||||
} catch (NoSuchMethodException e) {
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new RuntimeException(String.format("Setting '%s' does not have an accessible constructor", clazz.getName()), e);
|
||||
}
|
||||
})
|
||||
.map(constructor -> {
|
||||
try {
|
||||
return constructor.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
} catch(InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(String.format("Failed to create instance of setting '%s'", constructor.getDeclaringClass().getName()), e);
|
||||
}
|
||||
})
|
||||
@@ -82,7 +100,7 @@ public class Settings extends Appliance {
|
||||
}
|
||||
|
||||
public <T> T getSetting(Player player, Key key, Class<T> clazz) {
|
||||
Setting<?> setting = getSettings(player).stream()
|
||||
Setting<?> setting = this.getSettings(player).stream()
|
||||
.filter(s -> Objects.equals(s.getKey(), key))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
@@ -94,9 +112,21 @@ public class Settings extends Appliance {
|
||||
return clazz.cast(setting.state());
|
||||
}
|
||||
|
||||
/** Setzt einen Wert von außen (z. B. vom Proxy), ohne die Change-Listener auszulösen. */
|
||||
public <T> void setSetting(Player player, Key key, T value) {
|
||||
Setting<?> setting = this.getSettings(player).stream()
|
||||
.filter(s -> Objects.equals(s.getKey(), key))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
if(!setting.dataType().isInstance(value))
|
||||
throw new IllegalStateException("Tried to set Setting with Datatype " + value.getClass().getSimpleName() + " but expected " + setting.dataType().getSimpleName());
|
||||
setting.restore(player, value);
|
||||
}
|
||||
|
||||
public void openSettings(Player player) {
|
||||
List<Setting<?>> settings = getSettings(player);
|
||||
Inventory inventory = Bukkit.createInventory(null, calculateInvSize(settings), Component.text("Einstellungen"));
|
||||
List<Setting<?>> settings = this.getSettings(player);
|
||||
Inventory inventory = Bukkit.createInventory(null, this.calculateInvSize(settings), Component.text("Einstellungen"));
|
||||
|
||||
AtomicInteger row = new AtomicInteger(0);
|
||||
Arrays.stream(SettingCategory.values())
|
||||
@@ -106,13 +136,16 @@ public class Settings extends Appliance {
|
||||
.filter(setting -> ((CategorizedSetting) setting).category().equals(category))
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < categorizedSettings.size(); i++) {
|
||||
int slot = row.get() * 9 + i % 9;
|
||||
inventory.setItem(slot, categorizedSettings.get(i).buildItem());
|
||||
//skip empty category rows
|
||||
if(categorizedSettings.isEmpty()) return;
|
||||
|
||||
if (i % 9 == 8) {
|
||||
for(int i = 0; i < categorizedSettings.size(); i++) {
|
||||
if(i % 9 == 0 && i != 0) {
|
||||
row.incrementAndGet();
|
||||
}
|
||||
|
||||
int slot = row.get() * 9 + i % 9;
|
||||
inventory.setItem(slot, categorizedSettings.get(i).buildItem());
|
||||
}
|
||||
row.incrementAndGet();
|
||||
});
|
||||
@@ -121,16 +154,17 @@ public class Settings extends Appliance {
|
||||
.filter(setting -> !(setting instanceof CategorizedSetting))
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < uncategorizedSettings.size(); i++) {
|
||||
int slot = row.get() * 9 + i % 9;
|
||||
inventory.setItem(slot, uncategorizedSettings.get(i).buildItem());
|
||||
|
||||
if (i % 9 == 8) {
|
||||
for(int i = 0; i < uncategorizedSettings.size(); i++) {
|
||||
if(i % 9 == 0 && i != 0) {
|
||||
row.incrementAndGet();
|
||||
}
|
||||
|
||||
int slot = row.get() * 9 + i % 9;
|
||||
inventory.setItem(slot, uncategorizedSettings.get(i).buildItem());
|
||||
}
|
||||
|
||||
player.openInventory(inventory);
|
||||
InteractSounds.of(player).open();
|
||||
this.openSettingsInventories.put(player, new OpenSettingsInventory(inventory, settings));
|
||||
}
|
||||
|
||||
@@ -138,23 +172,30 @@ public class Settings extends Appliance {
|
||||
int countOfUncategorized = (int) settings.stream()
|
||||
.filter(setting -> !(setting instanceof CategorizedSetting))
|
||||
.count();
|
||||
int invSizeForUncategorized = (int) Math.ceil((double) countOfUncategorized / 9) * 9;
|
||||
|
||||
return Arrays.stream(SettingCategory.values())
|
||||
int invSizeForCategorized = Arrays.stream(SettingCategory.values())
|
||||
.map(settingCategory -> settings.stream()
|
||||
.filter(setting -> setting instanceof CategorizedSetting)
|
||||
.map(setting -> (CategorizedSetting) setting)
|
||||
.filter(categorizedSetting -> categorizedSetting.category().equals(settingCategory))
|
||||
.count())
|
||||
.map(itemCount -> itemCount + countOfUncategorized)
|
||||
.map(itemCount -> (int) Math.ceil((double) itemCount / 9))
|
||||
.reduce(Integer::sum)
|
||||
.orElse(1) * 9;
|
||||
|
||||
int invSize = invSizeForUncategorized + invSizeForCategorized;
|
||||
if(invSize % 9 != 0) throw new IllegalStateException(
|
||||
String.format("Failed to calculate settings inventory size. %d is not an multiple of 9", invSize)
|
||||
);
|
||||
return invSize;
|
||||
}
|
||||
|
||||
public void onSettingsClose(Player player) {
|
||||
if(!openSettingsInventories.containsKey(player)) return;
|
||||
openSettingsInventories.remove(player);
|
||||
if(!this.openSettingsInventories.containsKey(player)) return;
|
||||
this.openSettingsInventories.remove(player);
|
||||
player.updateInventory();
|
||||
InteractSounds.of(player).close();
|
||||
}
|
||||
|
||||
public boolean hasSettingsNotOpen(Player player) {
|
||||
@@ -162,7 +203,8 @@ public class Settings extends Appliance {
|
||||
}
|
||||
|
||||
public OpenSettingsInventory getOpenInventory(Player player) {
|
||||
if(hasSettingsNotOpen(player)) throw new RuntimeException("Cannot retrieve data from closed Settings inventory!");
|
||||
if(this.hasSettingsNotOpen(player))
|
||||
throw new RuntimeException("Cannot retrieve data from closed Settings inventory!");
|
||||
return this.openSettingsInventories.get(player);
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class SettingsCommand extends ApplianceCommand.PlayerChecked<Settings> {
|
||||
class SettingsCommand extends ApplianceCommand.PlayerChecked<Settings> {
|
||||
public SettingsCommand() {
|
||||
super("settings");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
|
||||
getAppliance().openSettings(getPlayer());
|
||||
this.getAppliance().openSettings(this.getPlayer());
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.BoolSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class SettingsShortcutSetting extends BoolSetting implements CategorizedSetting {
|
||||
@@ -25,7 +25,7 @@ public class SettingsShortcutSetting extends BoolSetting implements CategorizedS
|
||||
|
||||
@Override
|
||||
protected Boolean defaultValue() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
+8
-6
@@ -1,4 +1,4 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings.datatypes;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
@@ -17,14 +17,14 @@ public abstract class ActionSetting extends Setting<Void> {
|
||||
|
||||
@Override
|
||||
public ItemMeta buildMeta(ItemMeta meta) {
|
||||
meta.displayName(Component.text(title(), NamedTextColor.WHITE));
|
||||
meta.lore(buildDescription(description()));
|
||||
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
|
||||
meta.lore(this.buildDescription(this.description()));
|
||||
return meta;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void change(Player player, ClickType clickType) {
|
||||
onAction(player, clickType);
|
||||
this.onAction(player, clickType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,10 +33,12 @@ public abstract class ActionSetting extends Setting<Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fromStorage(PersistentDataContainer container) {}
|
||||
protected void fromStorage(PersistentDataContainer container) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toStorage(PersistentDataContainer container, Void value) {}
|
||||
protected void toStorage(PersistentDataContainer container, Void value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> dataType() {
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings.datatypes;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -20,19 +20,19 @@ public abstract class BoolSetting extends Setting<Boolean> {
|
||||
|
||||
@Override
|
||||
public void fromStorage(PersistentDataContainer container) {
|
||||
this.state = container.has(getNamespacedKey())
|
||||
? Objects.requireNonNull(container.get(getNamespacedKey(), PersistentDataType.BOOLEAN))
|
||||
: defaultValue();
|
||||
this.state = container.has(this.getNamespacedKey())
|
||||
? Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.BOOLEAN))
|
||||
: this.defaultValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toStorage(PersistentDataContainer container, Boolean value) {
|
||||
container.set(getNamespacedKey(), PersistentDataType.BOOLEAN, value);
|
||||
container.set(this.getNamespacedKey(), PersistentDataType.BOOLEAN, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemMeta buildMeta(ItemMeta meta) {
|
||||
meta.displayName(Component.text(title(), NamedTextColor.WHITE));
|
||||
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
|
||||
List<Component> lore = new ArrayList<>(List.of(
|
||||
Component.empty()
|
||||
.append(Component.text("Status: ", NamedTextColor.DARK_GRAY))
|
||||
@@ -42,7 +42,7 @@ public abstract class BoolSetting extends Setting<Boolean> {
|
||||
),
|
||||
Component.empty()
|
||||
));
|
||||
lore.addAll(buildDescription(description()));
|
||||
lore.addAll(this.buildDescription(this.description()));
|
||||
meta.lore(lore);
|
||||
return meta;
|
||||
}
|
||||
@@ -59,6 +59,6 @@ public abstract class BoolSetting extends Setting<Boolean> {
|
||||
|
||||
@Override
|
||||
public Boolean state() {
|
||||
return state;
|
||||
return this.state;
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public abstract class IntegerSetting extends Setting<Integer> {
|
||||
private final List<Integer> options;
|
||||
|
||||
public IntegerSetting(Settings.Key key, int minimum, int maximum) {
|
||||
this(key, IntStream.range(minimum, maximum+1).boxed().toList());
|
||||
}
|
||||
|
||||
public IntegerSetting(Settings.Key key, List<Integer> options) {
|
||||
super(key);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemMeta buildMeta(ItemMeta meta) {
|
||||
Component componentBefore = Component.text(" " + this.fillWithSpaces(this.options.getLast()));
|
||||
Component componentAfter = Component.text(" " + this.fillWithSpaces(this.options.getFirst()));
|
||||
int listIndex = this.options.indexOf(this.state);
|
||||
if(listIndex > 0) componentBefore = Component.text(" " + this.fillWithSpaces(this.options.get(listIndex-1)));
|
||||
if(listIndex < this.options.size()-1) componentAfter = Component.text(" " + this.fillWithSpaces(this.options.get(listIndex+1)));
|
||||
|
||||
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
|
||||
List<Component> lore = new ArrayList<>(Stream.of(
|
||||
Component.empty()
|
||||
.append(Component.text("Wert: ", NamedTextColor.DARK_GRAY)),
|
||||
Component.empty()
|
||||
.append(componentBefore.color(NamedTextColor.DARK_GRAY))
|
||||
.append(Component.text(" " + this.fillWithSpaces(this.state), NamedTextColor.GREEN))
|
||||
.append(componentAfter.color(NamedTextColor.DARK_GRAY)),
|
||||
Component.empty()
|
||||
).toList());
|
||||
lore.addAll(this.buildDescription(this.description()));
|
||||
meta.lore(lore);
|
||||
return meta;
|
||||
}
|
||||
|
||||
private String fillWithSpaces(Integer option) {
|
||||
String optionString = option.toString();
|
||||
int optionLength = optionString.length();
|
||||
int maxInteger = this.options.stream().mapToInt(value -> value).max().orElse(0);
|
||||
int maxLength = String.valueOf(maxInteger).length();
|
||||
int padding = maxLength - optionLength;
|
||||
|
||||
int padEnd = padding / 2;
|
||||
int padStart = padding - padEnd;
|
||||
|
||||
optionString = " ".repeat(padStart) + optionString + " ".repeat(padEnd);
|
||||
return optionString;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void change(Player player, ClickType clickType) {
|
||||
int elementBefore = this.options.getLast();
|
||||
int elementAfter = this.options.getFirst();
|
||||
int listIndex = this.options.indexOf(this.state);
|
||||
if(listIndex > 0) elementBefore = this.options.get(listIndex-1);
|
||||
if(listIndex < this.options.size()-1) elementAfter = this.options.get(listIndex+1);
|
||||
|
||||
if(clickType.equals(ClickType.LEFT)) this.state = elementBefore;
|
||||
if(clickType.equals(ClickType.RIGHT)) this.state = elementAfter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fromStorage(PersistentDataContainer container) {
|
||||
this.state = container.has(this.getNamespacedKey())
|
||||
? Integer.valueOf(Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.STRING)))
|
||||
: this.defaultValue();
|
||||
|
||||
if(!this.options.contains(this.state)) this.state = this.defaultValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toStorage(PersistentDataContainer container, Integer value) {
|
||||
container.set(this.getNamespacedKey(), PersistentDataType.STRING, new Gson().toJson(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> dataType() {
|
||||
return Integer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer state() {
|
||||
return this.state;
|
||||
}
|
||||
}
|
||||
+19
-17
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings.datatypes;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -35,9 +35,10 @@ public abstract class MultiBoolSetting<T> extends Setting<T> {
|
||||
|
||||
@Override
|
||||
public ItemMeta buildMeta(ItemMeta meta) {
|
||||
record SettingField(String name, String displayName, Boolean value) {}
|
||||
record SettingField(String name, String displayName, Boolean value) {
|
||||
}
|
||||
|
||||
meta.displayName(Component.text(title(), NamedTextColor.WHITE));
|
||||
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
|
||||
List<Component> lore = new ArrayList<>();
|
||||
lore.add(Component.text("Status: ", NamedTextColor.DARK_GRAY));
|
||||
|
||||
@@ -54,12 +55,12 @@ public abstract class MultiBoolSetting<T> extends Setting<T> {
|
||||
: component.getName();
|
||||
|
||||
return new SettingField(component.getName(), displayName, value);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
|
||||
} catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.map(field -> {
|
||||
if (cursorPosition == null) cursorPosition = field.name;
|
||||
if(this.cursorPosition == null) this.cursorPosition = field.name;
|
||||
boolean isSelected = field.name.equals(this.cursorPosition);
|
||||
return Component.text()
|
||||
.append(Component.text(
|
||||
@@ -79,7 +80,7 @@ public abstract class MultiBoolSetting<T> extends Setting<T> {
|
||||
.toList()
|
||||
);
|
||||
lore.add(Component.empty());
|
||||
lore.addAll(buildDescription(description()));
|
||||
lore.addAll(this.buildDescription(this.description()));
|
||||
lore.add(Component.empty());
|
||||
lore.add(Component.text("Linksklick", NamedTextColor.AQUA).append(Component.text(" zum Wählen der Option", NamedTextColor.GRAY)));
|
||||
lore.add(Component.text("Rechtsklick", NamedTextColor.AQUA).append(Component.text(" zum Ändern des Wertes", NamedTextColor.GRAY)));
|
||||
@@ -96,16 +97,16 @@ public abstract class MultiBoolSetting<T> extends Setting<T> {
|
||||
.findFirst()
|
||||
.orElse(-1);
|
||||
|
||||
if (clickType.equals(ClickType.LEFT)) {
|
||||
if(clickType.equals(ClickType.LEFT)) {
|
||||
currentIndex = (currentIndex + 1) % recordComponents.length;
|
||||
this.cursorPosition = recordComponents[currentIndex].getName();
|
||||
} else if (clickType.equals(ClickType.RIGHT)) {
|
||||
} else if(clickType.equals(ClickType.RIGHT)) {
|
||||
try {
|
||||
Object[] values = Arrays.stream(recordComponents)
|
||||
.map(rc -> {
|
||||
try {
|
||||
return this.state.getClass().getDeclaredMethod(rc.getName()).invoke(this.state);
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
} catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
@@ -119,7 +120,8 @@ public abstract class MultiBoolSetting<T> extends Setting<T> {
|
||||
this.state = (T) this.state.getClass().getConstructor(
|
||||
Arrays.stream(recordComponents).map(RecordComponent::getType).toArray(Class[]::new)
|
||||
).newInstance(values);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
|
||||
} catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException |
|
||||
InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@@ -127,21 +129,21 @@ public abstract class MultiBoolSetting<T> extends Setting<T> {
|
||||
|
||||
@Override
|
||||
protected void fromStorage(PersistentDataContainer container) {
|
||||
String data = container.has(getNamespacedKey())
|
||||
? Objects.requireNonNull(container.get(getNamespacedKey(), PersistentDataType.STRING))
|
||||
: new Gson().toJson(defaultValue());
|
||||
String data = container.has(this.getNamespacedKey())
|
||||
? Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.STRING))
|
||||
: new Gson().toJson(this.defaultValue());
|
||||
|
||||
try {
|
||||
//noinspection unchecked
|
||||
this.state = (T) new Gson().fromJson(data, dataType());
|
||||
this.state = (T) new Gson().fromJson(data, this.dataType());
|
||||
} catch(Exception e) {
|
||||
this.state = defaultValue();
|
||||
this.state = this.defaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toStorage(PersistentDataContainer container, T value) {
|
||||
container.set(getNamespacedKey(), PersistentDataType.STRING, new Gson().toJson(value));
|
||||
container.set(this.getNamespacedKey(), PersistentDataType.STRING, new Gson().toJson(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings.datatypes;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Material;
|
||||
@@ -37,7 +37,7 @@ public abstract class SelectSetting extends Setting<SelectSetting.Options.Option
|
||||
|
||||
@Override
|
||||
public ItemMeta buildMeta(ItemMeta meta) {
|
||||
meta.displayName(Component.text(title(), NamedTextColor.WHITE));
|
||||
meta.displayName(Component.text(this.title(), NamedTextColor.WHITE));
|
||||
List<Component> lore = new ArrayList<>();
|
||||
lore.add(Component.text("Status: ", NamedTextColor.DARK_GRAY));
|
||||
lore.addAll(
|
||||
@@ -52,7 +52,7 @@ public abstract class SelectSetting extends Setting<SelectSetting.Options.Option
|
||||
.toList()
|
||||
);
|
||||
lore.add(Component.empty());
|
||||
lore.addAll(buildDescription(description()));
|
||||
lore.addAll(this.buildDescription(this.description()));
|
||||
meta.lore(lore);
|
||||
return meta;
|
||||
}
|
||||
@@ -65,9 +65,9 @@ public abstract class SelectSetting extends Setting<SelectSetting.Options.Option
|
||||
.filter(i -> options.get(i).equals(this.state))
|
||||
.mapToObj(i -> {
|
||||
int nextIndex = i + optionModifier;
|
||||
if (nextIndex >= options.size()) {
|
||||
if(nextIndex >= options.size()) {
|
||||
return options.getFirst();
|
||||
} else if (nextIndex < 0) {
|
||||
} else if(nextIndex < 0) {
|
||||
return options.getLast();
|
||||
} else {
|
||||
return options.get(nextIndex);
|
||||
@@ -79,19 +79,19 @@ public abstract class SelectSetting extends Setting<SelectSetting.Options.Option
|
||||
|
||||
@Override
|
||||
protected void fromStorage(PersistentDataContainer container) {
|
||||
String data = container.has(getNamespacedKey())
|
||||
? Objects.requireNonNull(container.get(getNamespacedKey(), PersistentDataType.STRING))
|
||||
: defaultValue().key.asString();
|
||||
String data = container.has(this.getNamespacedKey())
|
||||
? Objects.requireNonNull(container.get(this.getNamespacedKey(), PersistentDataType.STRING))
|
||||
: this.defaultValue().key.asString();
|
||||
|
||||
this.state = this.options.options.stream()
|
||||
.filter(option -> option.key.asString().equals(data))
|
||||
.findFirst()
|
||||
.orElse(defaultValue());
|
||||
.orElse(this.defaultValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toStorage(PersistentDataContainer container, Options.Option value) {
|
||||
container.set(getNamespacedKey(), PersistentDataType.STRING, value.key.asString());
|
||||
container.set(this.getNamespacedKey(), PersistentDataType.STRING, value.key.asString());
|
||||
}
|
||||
|
||||
@Override
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Setting<TDataType> {
|
||||
protected TDataType state;
|
||||
private final Settings.Key key;
|
||||
|
||||
public Setting(Settings.Key key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public NamespacedKey getNamespacedKey() {
|
||||
return new NamespacedKey(Settings.class.getSimpleName().toLowerCase(), this.key.name().toLowerCase());
|
||||
}
|
||||
|
||||
public Settings.Key getKey() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
public void initializeFromPlayer(Player p) {
|
||||
PersistentDataContainer dataContainer = p.getPersistentDataContainer();
|
||||
try {
|
||||
this.fromStorage(dataContainer);
|
||||
} catch(IllegalArgumentException e) {
|
||||
Main.logger().warning(String.format(
|
||||
"Could not load state of setting %s from player %s: '%s'\n Did the datatype of the setting change?",
|
||||
this.getNamespacedKey(),
|
||||
e.getMessage(),
|
||||
p.getName()
|
||||
));
|
||||
dataContainer.remove(this.getNamespacedKey());
|
||||
this.fromStorage(dataContainer);
|
||||
Main.logger().info(String.format(
|
||||
"Restoring defaults of setting %s of player %s",
|
||||
this.getNamespacedKey(),
|
||||
p.getName()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public void triggerChange(Player p, ClickType clickType) {
|
||||
if(clickType.equals(ClickType.DOUBLE_CLICK)) return;
|
||||
this.change(p, clickType);
|
||||
InteractSounds.of(p).click();
|
||||
this.toStorage(p.getPersistentDataContainer(), this.state());
|
||||
Settings.instance().invokeChangeListener(p, this.getClass());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void restore(Player p, Object value) {
|
||||
this.state = (TDataType) value;
|
||||
this.toStorage(p.getPersistentDataContainer(), this.state);
|
||||
}
|
||||
|
||||
public ItemStack buildItem() {
|
||||
ItemStack stack = new ItemStack(this.icon(), 1);
|
||||
stack.setItemMeta(this.buildMeta(stack.getItemMeta()));
|
||||
return stack;
|
||||
}
|
||||
|
||||
protected List<TextComponent> buildDescription(String description) {
|
||||
return ComponentUtil.lineBreak(description, 50)
|
||||
.map(s -> Component.text(s, NamedTextColor.GRAY))
|
||||
.toList();
|
||||
}
|
||||
|
||||
protected abstract String title();
|
||||
protected abstract String description();
|
||||
protected abstract Material icon();
|
||||
public abstract ItemMeta buildMeta(ItemMeta meta);
|
||||
protected abstract void change(Player player, ClickType clickType);
|
||||
protected abstract TDataType defaultValue();
|
||||
protected abstract void fromStorage(PersistentDataContainer container);
|
||||
protected abstract void toStorage(PersistentDataContainer container, TDataType value);
|
||||
public abstract Class<?> dataType();
|
||||
public abstract TDataType state();
|
||||
}
|
||||
+6
-5
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings.listeners;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceListener;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
|
||||
|
||||
@@ -9,8 +9,9 @@ public class OpenSettingsShortcutListener extends ApplianceListener<Settings> {
|
||||
@EventHandler
|
||||
public void onItemSwitch(PlayerSwapHandItemsEvent event) {
|
||||
if(!event.getPlayer().isSneaking()) return;
|
||||
if(!Settings.instance().getSetting(event.getPlayer(), Settings.Key.EnableSettingsShortcut, Boolean.class)) return;
|
||||
if(!Settings.instance().getSetting(event.getPlayer(), Settings.Key.EnableSettingsShortcut, Boolean.class))
|
||||
return;
|
||||
event.setCancelled(true);
|
||||
getAppliance().openSettings(event.getPlayer());
|
||||
this.getAppliance().openSettings(event.getPlayer());
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.settings.listeners;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.listeners;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliance.ApplianceListener;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
@@ -11,10 +11,10 @@ public class SettingsInventoryListener extends ApplianceListener<Settings> {
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
Player player = (Player) event.getWhoClicked();
|
||||
if(getAppliance().hasSettingsNotOpen(player)) return;
|
||||
if(this.getAppliance().hasSettingsNotOpen(player)) return;
|
||||
event.setCancelled(true);
|
||||
|
||||
Settings.OpenSettingsInventory openInventory = getAppliance().getOpenInventory(player);
|
||||
Settings.OpenSettingsInventory openInventory = this.getAppliance().getOpenInventory(player);
|
||||
openInventory.settings().stream()
|
||||
.filter(setting -> setting.buildItem().equals(event.getCurrentItem()))
|
||||
.findFirst()
|
||||
@@ -26,6 +26,6 @@ public class SettingsInventoryListener extends ApplianceListener<Settings> {
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
getAppliance().onSettingsClose((Player) event.getPlayer());
|
||||
this.getAppliance().onSettingsClose((Player) event.getPlayer());
|
||||
}
|
||||
}
|
||||
+16
-15
@@ -1,13 +1,13 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.tablist;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.tablist;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.Main;
|
||||
import eu.mhsl.craftattack.spawn.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.appliances.report.Report;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.util.IteratorUtil;
|
||||
import eu.mhsl.craftattack.spawn.util.statistics.NetworkMonitor;
|
||||
import eu.mhsl.craftattack.spawn.util.text.ComponentUtil;
|
||||
import eu.mhsl.craftattack.spawn.util.text.RainbowComponent;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.report.Report;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.core.util.IteratorUtil;
|
||||
import eu.mhsl.craftattack.spawn.core.util.statistics.NetworkMonitor;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.ComponentUtil;
|
||||
import eu.mhsl.craftattack.spawn.core.util.text.RainbowComponent;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.util.Ticks;
|
||||
@@ -23,7 +23,8 @@ import java.util.List;
|
||||
|
||||
|
||||
public class Tablist extends Appliance {
|
||||
private final RainbowComponent serverName = new RainbowComponent(" CraftAttack 7 ", 7, 3);
|
||||
private final String projectTitle = this.localConfig().getString("projectTitle", "Title not configured");
|
||||
private final RainbowComponent serverName = new RainbowComponent(String.format(" %s ", this.projectTitle), 7, 3);
|
||||
private NetworkMonitor networkMonitor;
|
||||
private OperatingSystemMXBean systemMonitor;
|
||||
|
||||
@@ -36,7 +37,7 @@ public class Tablist extends Appliance {
|
||||
Settings.instance().declareSetting(TechnicalTablistSetting.class);
|
||||
|
||||
int tabRefreshRate = 3;
|
||||
this.networkMonitor = new NetworkMonitor(localConfig().getString("interface"), Duration.ofSeconds(1));
|
||||
this.networkMonitor = new NetworkMonitor(this.localConfig().getString("interface"), Duration.ofSeconds(1));
|
||||
this.systemMonitor = ManagementFactory.getOperatingSystemMXBean();
|
||||
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(
|
||||
@@ -53,14 +54,14 @@ public class Tablist extends Appliance {
|
||||
}
|
||||
|
||||
public void fullUpdate(Player player) {
|
||||
updateHeader(player);
|
||||
updateFooter(player);
|
||||
this.updateHeader(player);
|
||||
this.updateFooter(player);
|
||||
}
|
||||
|
||||
private void updateHeader(Player player) {
|
||||
boolean detailedInfo = queryAppliance(Settings.class).getSetting(player, Settings.Key.TechnicalTab, Boolean.class);
|
||||
boolean detailedInfo = this.queryAppliance(Settings.class).getSetting(player, Settings.Key.TechnicalTab, Boolean.class);
|
||||
Component header = Component.newline()
|
||||
.append(serverName.getRainbowState()).appendNewline()
|
||||
.append(this.serverName.getRainbowState()).appendNewline()
|
||||
.append(Component.text("mhsl.eu", NamedTextColor.GOLD)).appendNewline().appendNewline()
|
||||
.append(ComponentUtil.getFormattedTickTimes(detailedInfo)).appendNewline();
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.tablist;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
class TablistListener extends ApplianceListener<Tablist> {
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
this.getAppliance().fullUpdate(event.getPlayer());
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package eu.mhsl.craftattack.spawn.appliances.tablist;
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.tablist;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.BoolSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class TechnicalTablistSetting extends BoolSetting implements CategorizedSetting {
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiAutoTotem;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class AntiAutoTotem extends Appliance {
|
||||
|
||||
public void checkTotemUse(Player player) {
|
||||
PlayerInventory playerInv = player.getInventory();
|
||||
|
||||
Supplier<List<Material>> getHeldItems = () -> List.of(
|
||||
playerInv.getItemInMainHand().getType(),
|
||||
playerInv.getItemInOffHand().getType()
|
||||
);
|
||||
Function<Player, Boolean> isCurrentlyHoldingTotem = (p) -> getHeldItems.get().contains(Material.TOTEM_OF_UNDYING);
|
||||
|
||||
if(!isCurrentlyHoldingTotem.apply(player)) return;
|
||||
if(getHeldItems.get().stream().allMatch(material -> material.equals(Material.TOTEM_OF_UNDYING))) return;
|
||||
|
||||
AtomicInteger tickCounter = new AtomicInteger();
|
||||
Bukkit.getScheduler().runTaskTimer(
|
||||
Main.instance(),
|
||||
(task) -> {
|
||||
if(tickCounter.incrementAndGet() > 10) {
|
||||
task.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if(isCurrentlyHoldingTotem.apply(player)) {
|
||||
task.cancel();
|
||||
Main.instance().getAppliance(AcInform.class).notifyAdmins(
|
||||
"internal",
|
||||
player.getName(),
|
||||
"antiAutoTotem",
|
||||
(float) tickCounter.get()
|
||||
);
|
||||
}
|
||||
},
|
||||
1,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new OnTotemUseListener()
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiAutoTotem;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.entity.EntityResurrectEvent;
|
||||
|
||||
class OnTotemUseListener extends ApplianceListener<AntiAutoTotem> {
|
||||
@EventHandler
|
||||
public void onTotem(EntityResurrectEvent event) {
|
||||
if(event.isCancelled()) return;
|
||||
if(!(event.getEntity() instanceof Player player)) return;
|
||||
this.getAppliance().checkTotemUse(player);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiBoatFreecam;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Boat;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Appliance.Flags(enabled = false)
|
||||
public class AntiBoatFreecam extends Appliance {
|
||||
private static final float MAX_YAW_OFFSET = 106.0f;
|
||||
|
||||
public AntiBoatFreecam() {
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(
|
||||
Main.instance(),
|
||||
() -> Bukkit.getOnlinePlayers().forEach(player -> {
|
||||
if(!(player.getVehicle() instanceof Boat boat)) return;
|
||||
if(!boat.getPassengers().getFirst().equals(player)) return;
|
||||
float playerYaw = player.getYaw();
|
||||
float boatYaw = boat.getYaw();
|
||||
|
||||
float yawDelta = wrapDegrees(playerYaw - boatYaw);
|
||||
if(Math.abs(yawDelta) <= MAX_YAW_OFFSET) return;
|
||||
|
||||
Main.instance().getAppliance(AcInform.class).slowedNotifyAdmins(
|
||||
"internal",
|
||||
player.getName(),
|
||||
"illegalBoatLookYaw",
|
||||
yawDelta,
|
||||
3000
|
||||
);
|
||||
}),
|
||||
1L,
|
||||
1L
|
||||
);
|
||||
}
|
||||
|
||||
private static float wrapDegrees(float deg) {
|
||||
deg = deg % 360f;
|
||||
if (deg >= 180f) deg -= 360f;
|
||||
if (deg < -180f) deg += 360f;
|
||||
return deg;
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiFormattedBook;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import net.kyori.adventure.text.*;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.meta.BookMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class AntiFormattedBook extends Appliance {
|
||||
private static final char SECTION = '\u00A7';
|
||||
|
||||
public boolean containsFormatting(BookMeta meta) {
|
||||
if (this.hasFormattingDeep(meta.title())) return true;
|
||||
if (this.hasFormattingDeep(meta.author())) return true;
|
||||
|
||||
for (Component c : meta.pages()) {
|
||||
if (this.hasFormattingDeep(c)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasFormattingDeep(@Nullable Component component) {
|
||||
if(component == null) return false;
|
||||
if (this.hastFormatting(component)) return true;
|
||||
|
||||
if (component instanceof TextComponent tc && tc.content().indexOf(SECTION) >= 0) return true;
|
||||
|
||||
if (component instanceof NBTComponent<?> nbt) {
|
||||
if (nbt.separator() != null && this.hasFormattingDeep(nbt.separator())) return true;
|
||||
}
|
||||
|
||||
for (Component child : component.children()) {
|
||||
if (this.hasFormattingDeep(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hastFormatting(Component component) {
|
||||
Style style = component.style();
|
||||
|
||||
TextColor color = style.color();
|
||||
if (color != null) return true;
|
||||
if (style.font() != null) return true;
|
||||
if (style.insertion() != null && !Objects.requireNonNull(style.insertion()).isEmpty()) return true;
|
||||
|
||||
for (var decoration : style.decorations().entrySet()) {
|
||||
if (decoration.getValue() == TextDecoration.State.TRUE) return true;
|
||||
}
|
||||
|
||||
return style.hoverEvent() != null || style.clickEvent() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new BookEditListener()
|
||||
);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiFormattedBook;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerEditBookEvent;
|
||||
import org.bukkit.inventory.meta.BookMeta;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class BookEditListener extends ApplianceListener<AntiFormattedBook> {
|
||||
@EventHandler
|
||||
public void onBookEdit(PlayerEditBookEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
BookMeta meta = event.getNewBookMeta();
|
||||
|
||||
if (this.getAppliance().containsFormatting(meta)) {
|
||||
Main.instance().getAppliance(AcInform.class).notifyAdmins(
|
||||
"internal",
|
||||
player.getName(),
|
||||
"illegalBookFormatting",
|
||||
1f
|
||||
);
|
||||
|
||||
BookMeta sanitized = meta.clone();
|
||||
sanitized.title(null);
|
||||
sanitized.author(null);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
sanitized.pages(List.of(Component.empty()));
|
||||
event.setNewBookMeta(sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalBundlePicker;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.InventoryView;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.BundleMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AntiIllegalBundlePicker extends Appliance {
|
||||
private static final int visibleSlotsInBundle = 9;
|
||||
|
||||
public void trackBundle(InventoryClickEvent event) {
|
||||
ItemStack bundle = Objects.requireNonNull(event.getCurrentItem());
|
||||
final int rawSlot = event.getRawSlot();
|
||||
final Player player = (Player) event.getWhoClicked();
|
||||
final InventoryView view = event.getView();
|
||||
final List<ItemStack> before = this.getBundleContents(bundle);
|
||||
|
||||
Bukkit.getScheduler().runTask(Main.instance(), () -> {
|
||||
ItemStack afterStack = view.getItem(rawSlot);
|
||||
if(afterStack == null || afterStack.getType() != Material.BUNDLE) return;
|
||||
|
||||
List<ItemStack> after = this.getBundleContents(afterStack);
|
||||
int removedSlotIndex = this.findRemoved(before, after);
|
||||
|
||||
if(removedSlotIndex >= visibleSlotsInBundle) {
|
||||
Main.instance().getAppliance(AcInform.class).notifyAdmins(
|
||||
"internal",
|
||||
player.getName(),
|
||||
"illegalBundlePick",
|
||||
(float) removedSlotIndex
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int findRemoved(@NotNull List<ItemStack> before, @NotNull List<ItemStack> after) {
|
||||
for (int i = 0; i < Math.max(before.size(), after.size()); i++) {
|
||||
ItemStack a = i < after.size() ? after.get(i) : null;
|
||||
ItemStack b = i < before.size() ? before.get(i) : null;
|
||||
|
||||
if (b == null && a == null) continue;
|
||||
if (b == null) throw new IllegalStateException("Size of bundle was smaller before pickup Action");
|
||||
|
||||
if (a == null) return i;
|
||||
if (!a.isSimilar(b)) return i;
|
||||
if (a.getAmount() != b.getAmount()) return i;
|
||||
}
|
||||
throw new IllegalStateException("Failed to find picked Item in bundle");
|
||||
}
|
||||
|
||||
private List<ItemStack> getBundleContents(@NotNull ItemStack bundle) {
|
||||
if (bundle.getType() != Material.BUNDLE)
|
||||
throw new IllegalStateException("ItemStack is not a bundle");
|
||||
|
||||
BundleMeta meta = (BundleMeta) bundle.getItemMeta();
|
||||
return meta.getItems().stream()
|
||||
.map(ItemStack::clone)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new OnBundlePickListener()
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalBundlePicker;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.inventory.InventoryAction;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
class OnBundlePickListener extends ApplianceListener<AntiIllegalBundlePicker> {
|
||||
@EventHandler
|
||||
public void onBundlePick(InventoryClickEvent event) {
|
||||
if(!event.getAction().equals(InventoryAction.PICKUP_FROM_BUNDLE)) return;
|
||||
final ItemStack bundle = event.getCurrentItem();
|
||||
if (bundle == null || bundle.getType() != Material.BUNDLE) return;
|
||||
this.getAppliance().trackBundle(event);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalSignCharacters;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AntiIllegalSignCharacters extends Appliance {
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new SignEditListener()
|
||||
);
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalSignCharacters;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.block.SignChangeEvent;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.Set;
|
||||
|
||||
class SignEditListener extends ApplianceListener<AntiIllegalSignCharacters> {
|
||||
private static final Set<Integer> ALLOWED_CHARS = Set.of(
|
||||
(int)' ', (int)'.', (int)',', (int)';', (int)':', (int)'!', (int)'?',
|
||||
(int)'"', (int)'\'',
|
||||
(int)'(', (int)')', (int)'[', (int)']', (int)'{', (int)'}',
|
||||
(int)'-', (int)'_', (int)'+', (int)'=', (int)'/', (int)'\\',
|
||||
(int)'@', (int)'#', (int)'$', (int)'%', (int)'&', (int)'*',
|
||||
(int)'<', (int)'>', (int)'|',
|
||||
(int)'~', (int)'`', (int)'^'
|
||||
);
|
||||
|
||||
private static final Set<Integer> ALLOWED_EXTRA = Set.of(
|
||||
(int)'Ä', (int)'Ö', (int)'Ü', (int)'ä', (int)'ö', (int)'ü', (int)'ß',
|
||||
(int)'€', (int)'°', (int)'µ'
|
||||
);
|
||||
|
||||
@EventHandler
|
||||
public void onSignEdit(SignChangeEvent event) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Component line = event.line(i);
|
||||
if (line == null) continue;
|
||||
String plainString = PlainTextComponentSerializer.plainText().serialize(line);
|
||||
plainString = Normalizer.normalize(plainString, Normalizer.Form.NFC);
|
||||
String cleaned = filterAllowed(plainString);
|
||||
event.line(i, Component.text(cleaned));
|
||||
}
|
||||
}
|
||||
|
||||
private static String filterAllowed(String s) {
|
||||
StringBuilder out = new StringBuilder(s.length());
|
||||
|
||||
for (int off = 0; off < s.length(); ) {
|
||||
int cp = s.codePointAt(off);
|
||||
off += Character.charCount(cp);
|
||||
|
||||
if (isForbidden(cp)) continue;
|
||||
|
||||
if (Character.isLetterOrDigit(cp)) {
|
||||
out.appendCodePoint(cp);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ALLOWED_CHARS.contains(cp) || ALLOWED_EXTRA.contains(cp)) {
|
||||
out.appendCodePoint(cp);
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static boolean isForbidden(int cp) {
|
||||
// Surrogates / invalid
|
||||
if (cp >= 0xD800 && cp <= 0xDFFF) return true;
|
||||
|
||||
// Private Use Area (Mod/Pack-Icons/Placeholder)
|
||||
if (cp >= 0xE000 && cp <= 0xF8FF) return true;
|
||||
|
||||
// Zero-width and control characters
|
||||
if (cp == 0x200B || cp == 0x200C || cp == 0x200D || cp == 0xFEFF) return true;
|
||||
|
||||
// BiDi-Steuerzeichen
|
||||
if (cp >= 0x202A && cp <= 0x202E) return true;
|
||||
if (cp >= 0x2066 && cp <= 0x2069) return true;
|
||||
|
||||
return cp == '§';
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import net.kyori.adventure.util.Ticks;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Appliance.Flags(enabled = false)
|
||||
public class AntiInventoryMove extends Appliance {
|
||||
private static final long errorTimeMargin = Ticks.SINGLE_TICK_DURATION_MS * 2;
|
||||
|
||||
private final Map<UUID, Long> invOpen = new ConcurrentHashMap<>();
|
||||
|
||||
public void setInvOpen(Player player, boolean open) {
|
||||
if(open)
|
||||
this.invOpen.put(player.getUniqueId(), System.currentTimeMillis());
|
||||
else
|
||||
this.invOpen.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
public boolean hasInventoryOpen(Player player) {
|
||||
if(!this.invOpen.containsKey(player.getUniqueId())) return false;
|
||||
return this.invOpen.get(player.getUniqueId()) < System.currentTimeMillis() - errorTimeMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(
|
||||
new InventoryTrackerListener(),
|
||||
new InInventoryMoveListener()
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerInputEvent;
|
||||
|
||||
class InInventoryMoveListener extends ApplianceListener<AntiInventoryMove> {
|
||||
@EventHandler
|
||||
public void onInput(PlayerInputEvent event) {
|
||||
if(!this.getAppliance().hasInventoryOpen(event.getPlayer())) return;
|
||||
Main.instance().getAppliance(AcInform.class).slowedNotifyAdmins(
|
||||
"internal",
|
||||
event.getPlayer().getName(),
|
||||
"inInventoryMove",
|
||||
-1f,
|
||||
3000
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.security.antiInventoryMove;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
|
||||
class InventoryTrackerListener extends ApplianceListener<AntiInventoryMove> {
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onOpen(InventoryOpenEvent event) {
|
||||
if(!(event.getPlayer() instanceof Player player)) return;
|
||||
if(event.isCancelled()) return;
|
||||
this.getAppliance().setInvOpen(player, true);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onClose(InventoryCloseEvent event) {
|
||||
if(!(event.getPlayer() instanceof Player player)) return;
|
||||
this.getAppliance().setInvOpen(player, false);
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentBuilder;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AcInform extends Appliance {
|
||||
private final Map<String, Map<String, Long>> violationSlowdowns = new ConcurrentHashMap<>();
|
||||
|
||||
public void processCommand(@NotNull String[] args) {
|
||||
String anticheatName = null;
|
||||
String playerName = null;
|
||||
String checkName = null;
|
||||
Float violationCount = null;
|
||||
int notifyEvery = 0;
|
||||
|
||||
for(int i = 0; i < args.length; i++) {
|
||||
if(!args[i].startsWith("--")) continue;
|
||||
|
||||
StringBuilder valueBuilder = new StringBuilder();
|
||||
for(int j = i + 1; j < args.length; j++) {
|
||||
if(args[j].startsWith("--")) break;
|
||||
if(!valueBuilder.isEmpty()) valueBuilder.append(" ");
|
||||
valueBuilder.append(args[j]);
|
||||
}
|
||||
|
||||
String value = valueBuilder.toString();
|
||||
switch(args[i]) {
|
||||
case "--anticheatName" -> anticheatName = value;
|
||||
case "--playerName" -> playerName = value;
|
||||
case "--check" -> checkName = value;
|
||||
case "--violationCount" -> violationCount = value.isEmpty() ? null : Float.valueOf(value);
|
||||
case "--notifyEvery" -> notifyEvery = Integer.parseInt(value);
|
||||
}
|
||||
}
|
||||
|
||||
if(notifyEvery == 0) {
|
||||
this.notifyAdmins(anticheatName, playerName, checkName, violationCount);
|
||||
} else {
|
||||
this.slowedNotifyAdmins(anticheatName, playerName, checkName, violationCount, notifyEvery);
|
||||
}
|
||||
}
|
||||
|
||||
public void slowedNotifyAdmins(@Nullable String anticheatName, @Nullable String playerName, @Nullable String checkName, @Nullable Float violationCount, int notifyEvery) {
|
||||
this.violationSlowdowns.putIfAbsent(playerName, new HashMap<>());
|
||||
|
||||
var slowdowns = this.violationSlowdowns.get(playerName);
|
||||
if(slowdowns.containsKey(checkName)) {
|
||||
if(slowdowns.get(checkName) > System.currentTimeMillis() - notifyEvery) return;
|
||||
}
|
||||
|
||||
this.notifyAdmins(anticheatName, playerName, checkName, violationCount);
|
||||
}
|
||||
|
||||
public void notifyAdmins(@Nullable String anticheatName, @Nullable String playerName, @Nullable String checkName, @Nullable Float violationCount) {
|
||||
this.violationSlowdowns.putIfAbsent(playerName, new HashMap<>());
|
||||
this.violationSlowdowns.get(playerName).put(checkName, System.currentTimeMillis());
|
||||
|
||||
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text();
|
||||
NamedTextColor textColor = NamedTextColor.GRAY;
|
||||
|
||||
if(playerName == null || playerName.isBlank())
|
||||
throw new ApplianceCommand.Error("acinform command needs a player (--playerName)");
|
||||
|
||||
if(anticheatName != null && !anticheatName.isBlank()) {
|
||||
component
|
||||
.append(Component.text("⊤ ", NamedTextColor.GRAY))
|
||||
.append(Component.text("[", textColor))
|
||||
.append(Component.text(anticheatName, NamedTextColor.RED))
|
||||
.append(Component.text("]: ", textColor));
|
||||
}
|
||||
|
||||
component
|
||||
.append(Component.text("Player ", textColor))
|
||||
.append(Component.text(playerName, NamedTextColor.WHITE))
|
||||
.append(Component.text(" "));
|
||||
|
||||
if(checkName == null || checkName.isBlank()) {
|
||||
component.append(Component.text("got detected by Anticheat", textColor));
|
||||
} else if(violationCount != null) {
|
||||
component.append(
|
||||
Component.text("failed ", textColor)
|
||||
.append(Component.text(String.format("%sx ", violationCount), NamedTextColor.WHITE))
|
||||
.append(Component.text("'", textColor))
|
||||
.append(Component.text(checkName, NamedTextColor.WHITE))
|
||||
.append(Component.text("'", textColor))
|
||||
);
|
||||
} else {
|
||||
component.append(
|
||||
Component.text("failed ", textColor)
|
||||
.append(Component.text("'", textColor))
|
||||
.append(Component.text(checkName, NamedTextColor.WHITE))
|
||||
.append(Component.text("'", textColor))
|
||||
);
|
||||
}
|
||||
|
||||
component.append(
|
||||
Component.newline()
|
||||
.append(Component.text("⊥ ", NamedTextColor.GRAY))
|
||||
.append(Component.text("[", NamedTextColor.GRAY))
|
||||
.append(Component.text("\uD83D\uDCD6", NamedTextColor.GOLD))
|
||||
.append(Component.text("]", NamedTextColor.GRAY))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/report %s anticheat %s flagged %s", playerName, anticheatName, checkName)))
|
||||
);
|
||||
|
||||
component.append(
|
||||
Component.text(" [", NamedTextColor.GRAY)
|
||||
.append(Component.text("\u23F1", NamedTextColor.GOLD))
|
||||
.append(Component.text("]", NamedTextColor.GRAY))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/kick %s", playerName)))
|
||||
);
|
||||
|
||||
component.append(
|
||||
Component.text(" [", NamedTextColor.GRAY)
|
||||
.append(Component.text("\uD83E\uDDB6", NamedTextColor.GOLD))
|
||||
.append(Component.text("]", NamedTextColor.GRAY))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/kickunsuspected %s", playerName)))
|
||||
);
|
||||
|
||||
component.append(
|
||||
Component.text(" [", NamedTextColor.GRAY)
|
||||
.append(Component.text("\u2623", NamedTextColor.GOLD))
|
||||
.append(Component.text("]", NamedTextColor.GRAY))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/kickcrash %s", playerName)))
|
||||
);
|
||||
|
||||
component.append(
|
||||
Component.text(" [", NamedTextColor.GRAY)
|
||||
.append(Component.text("\uD83D\uDD12", NamedTextColor.GOLD))
|
||||
.append(Component.text("]", NamedTextColor.GRAY))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/panicban %s", playerName)))
|
||||
);
|
||||
|
||||
component.append(
|
||||
Component.text(" [", NamedTextColor.GRAY)
|
||||
.append(Component.text("\uD83D\uDC41", NamedTextColor.GOLD))
|
||||
.append(Component.text("]", NamedTextColor.GRAY))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/grim spectate %s", playerName)))
|
||||
);
|
||||
|
||||
TextComponent finalMessage = component.build();
|
||||
|
||||
Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> player.hasPermission("admin"))
|
||||
.forEach(player -> player.sendMessage(finalMessage));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<ApplianceCommand<?>> commands() {
|
||||
return List.of(
|
||||
new AcInformCommand()
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 469 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user