implemented PlayTimer

This commit is contained in:
2025-06-15 18:42:49 +02:00
parent 69e971f618
commit fce9449b7e
5 changed files with 273 additions and 4 deletions

View File

@ -0,0 +1,95 @@
package eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.playTimer;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
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.varo.appliances.metaGameplay.teams.VaroTeam;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PlayTimer extends Appliance {
public static final int PLAYTIME_MINUTES = 30;
private final Map<String, Integer> joinTickets = new HashMap<>();
private final Path saveFile = Paths.get(Main.instance().getDataFolder() + "playtime.json");
public PlayTimer() {
this.load();
}
private void load() {
if (!Files.exists(this.saveFile)) return;
try (Reader reader = Files.newBufferedReader(this.saveFile)) {
Type type = new TypeToken<Map<String, Object>>() {}.getType();
Map<String, Object> data = new Gson().fromJson(reader, type);
@SuppressWarnings("unchecked") Map<String, Double> ticketMap = (Map<String, Double>) data.get("tickets");
if (ticketMap != null) {
for (Map.Entry<String, Double> entry : ticketMap.entrySet()) {
this.joinTickets.put(entry.getKey(), entry.getValue().intValue());
}
}
} catch (IOException e) {
Main.logger().warning("Failed reading playtime from teams: " + e.getMessage());
}
}
private void save() {
try {
Files.createDirectories(this.saveFile.getParent());
try (Writer writer = Files.newBufferedWriter(this.saveFile)) {
new Gson().toJson(Map.of(
"tickets", this.joinTickets
), writer);
}
} catch (IOException e) {
Main.logger().warning("Failed to save playtime for teams: " + e.getMessage());
}
}
public void incrementAll() {
this.joinTickets.replaceAll((n, v) -> this.joinTickets.get(n) + 1);
this.save();
}
public void setTickets(VaroTeam team, int amount) {
this.joinTickets.put(team.name, amount);
}
public int getTickets(VaroTeam team) {
return this.joinTickets.getOrDefault(team.name, 1);
}
public boolean tryConsumeTicket(VaroTeam team) {
String teamName = team.name;
int current = this.joinTickets.getOrDefault(teamName, 1);
if (current <= 0) return false;
this.joinTickets.put(teamName, current - 1);
this.save();
return true;
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new PlayTimerCommand());
}
}

View File

@ -0,0 +1,98 @@
package eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.playTimer;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.teams.Teams;
import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.teams.VaroTeam;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Stream;
public class PlayTimerCommand extends ApplianceCommand<PlayTimer> {
public PlayTimerCommand() {
super("playTimer");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
if (args.length < 3) throw new Error("Usage: playTimer <user|team> <identifier> <get|set> [amount]");
String mode = args[0].toLowerCase();
String identifier = args[1];
String action = args[2].toLowerCase();
Teams teamAppliance = Main.instance().getAppliance(Teams.class);
VaroTeam team = switch (mode) {
case "user" -> {
OfflinePlayer player = Bukkit.getOfflinePlayer(identifier);
try {
yield teamAppliance.getTeamFromPlayer(player.getUniqueId());
} catch(NoSuchElementException e) {
throw new Error("Dieser Spieler konnte keinem Team zugeordnet werden!");
}
}
case "team" -> {
VaroTeam targetTeam = teamAppliance.findTeamByName(identifier);
if (targetTeam == null) throw new Error("Team nicht gefunden.");
yield targetTeam;
}
default -> throw new Error("Ungültiger Modus: " + mode + ". Erlaubt: user | team");
};
switch (action) {
case "get" -> {
int ticketCount = this.getAppliance().getTickets(team);
sender.sendMessage(String.format("Team %s hat %d tickets!", team.name, ticketCount));
}
case "set" -> {
if (args.length < 4) throw new Error("Usage: playTimer <user|team> <identifier> set <amount>");
int amount = Integer.parseInt(args[3]);
this.getAppliance().setTickets(team, amount);
sender.sendMessage("Tickets wurden gesetzt!");
}
default -> throw new Error("Ungültige Aktion: " + action + ". Erlaubt: get | set");
}
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
List<VaroTeam> teams = Main.instance().getAppliance(Teams.class).getAllTeams();
return switch (args.length) {
case 1 -> Stream.of("user", "team")
.filter(opt -> opt.startsWith(args[0].toLowerCase()))
.toList();
case 2 -> {
if (args[0].equalsIgnoreCase("user")) {
yield Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase()))
.toList();
} else if (args[0].equalsIgnoreCase("team")) {
yield teams.stream()
.map(team -> team.name)
.filter(name -> name.toLowerCase().startsWith(args[1].toLowerCase()))
.toList();
} else {
yield List.of();
}
}
case 3 -> Stream.of("get", "set")
.filter(opt -> opt.startsWith(args[2].toLowerCase()))
.toList();
default -> List.of();
};
}
}

View File

@ -30,10 +30,15 @@ class ConnectivityChangeListener extends ApplianceListener<Teams> {
public void onLeave(PlayerQuitEvent event) { public void onLeave(PlayerQuitEvent event) {
if(event.getReason().equals(PlayerQuitEvent.QuitReason.KICKED)) { if(event.getReason().equals(PlayerQuitEvent.QuitReason.KICKED)) {
Main.logger().info(String.format( Main.logger().info(String.format(
"Player %s left the Server. The 'teamLeave' enforcement was skipped, since the QuitReqson is 'kicked'", "Player %s left the Server. The 'teamLeave' enforcement was skipped, since the QuitReason is 'kicked'",
event.getPlayer().getName() event.getPlayer().getName()
)); ));
return;
} }
VaroTeam team = Main.instance().getAppliance(Teams.class).getTeamFromPlayer(event.getPlayer().getUniqueId());
Main.logger().info(String.format("Team %s got a Strike, because they %s left early!", team.name, event.getPlayer().getName()));
// TODO: strike team
this.getAppliance().enforceTeamLeave(event.getPlayer()); this.getAppliance().enforceTeamLeave(event.getPlayer());
} }
} }

View File

@ -6,6 +6,7 @@ import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName.DisplayName; import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName.DisplayName;
import eu.mhsl.craftattack.spawn.varo.api.repositories.TeamRepository; import eu.mhsl.craftattack.spawn.varo.api.repositories.TeamRepository;
import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.joinProtection.JoinProtection; import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.joinProtection.JoinProtection;
import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.playTimer.PlayTimer;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
@ -56,7 +57,7 @@ public class Teams extends Appliance implements DisplayName.Prefixed, DisplayNam
} }
} }
private @Nullable VaroTeam findTeamByName(String name) { public @Nullable VaroTeam findTeamByName(String name) {
for (VaroTeam team : this.teams) { for (VaroTeam team : this.teams) {
if (team.name.equals(name)) return team; if (team.name.equals(name)) return team;
} }
@ -75,15 +76,51 @@ public class Teams extends Appliance implements DisplayName.Prefixed, DisplayNam
.orElseThrow(); .orElseThrow();
} }
public List<VaroTeam> getAllTeams() {
return this.teams;
}
public void enforceTeamJoin(Player joinedPlayer) { public void enforceTeamJoin(Player joinedPlayer) {
DisconnectInfo disconnectInfo = new DisconnectInfo( DisconnectInfo teamNotCompleteInfo = new DisconnectInfo(
"Teampartner nicht beigetreten", "Teampartner nicht beigetreten",
"Deine Verbindung wurde getrennt, da dein Teampartner keine Verbindung zum Server hergestellt hat!", "Deine Verbindung wurde getrennt, da dein Teampartner keine Verbindung zum Server hergestellt hat!",
"Bitte sorge dafür, dass alle anderen Teammitglieder eine einwandfreie Internetverbindung haben und melde dich im Zweifel bei einem Admin!", "Bitte sorge dafür, dass alle anderen Teammitglieder eine einwandfreie Internetverbindung haben und melde dich im Zweifel bei einem Admin!",
joinedPlayer.getUniqueId() joinedPlayer.getUniqueId()
); );
DisconnectInfo teamNoPlaytime = new DisconnectInfo(
"Keine Spielzeit verfügbar",
"Deine Verbindung wurde getrennt, da dein Team keine verbleibende Spielzeit auf dem Server hat!",
"Falls dies ein Fehler ist, melde dich bitte bei einem Admin!",
joinedPlayer.getUniqueId()
);
VaroTeam team = this.getTeamFromPlayer(joinedPlayer.getUniqueId()); VaroTeam team = this.getTeamFromPlayer(joinedPlayer.getUniqueId());
PlayTimer playTimer = Main.instance().getAppliance(PlayTimer.class);
boolean isAllowed = playTimer.tryConsumeTicket(team);
if(!isAllowed) {
Main.logger().warning(String.format("Team %s joined, but got denied from Ticketing. Team will be kicked!", team.name));
team.kickTeam(teamNoPlaytime);
return;
}
int leftTickets = playTimer.getTickets(team);
Main.logger().info(String.format("Player %s joined. There are %d tickets left!", joinedPlayer.getName(), leftTickets));
String playtimeOverview = String.format(
"Dein Team hat noch %d Beitritte, also %dx%d=%d Minuten übrig.",
leftTickets,
leftTickets,
PlayTimer.PLAYTIME_MINUTES,
leftTickets * PlayTimer.PLAYTIME_MINUTES
);
joinedPlayer.sendMessage(Component.text(
leftTickets == 0
? String.format("Dein Team hat ab jetzt %d Minuten Spielzeit!", PlayTimer.PLAYTIME_MINUTES)
: playtimeOverview,
NamedTextColor.GREEN
));
Bukkit.getScheduler().scheduleSyncDelayedTask( Bukkit.getScheduler().scheduleSyncDelayedTask(
Main.instance(), Main.instance(),
() -> team.members.stream() () -> team.members.stream()
@ -93,7 +130,14 @@ public class Teams extends Appliance implements DisplayName.Prefixed, DisplayNam
return p == null || !p.isOnline(); return p == null || !p.isOnline();
}) })
.findAny() .findAny()
.ifPresent(member -> team.kickTeam(disconnectInfo)), .ifPresentOrElse(
member -> team.kickTeam(teamNotCompleteInfo),
() -> Bukkit.getScheduler().runTaskLater(
Main.instance(),
team::timeOverKick,
Ticks.TICKS_PER_SECOND * 60 * PlayTimer.PLAYTIME_MINUTES
)
),
Ticks.TICKS_PER_SECOND * (JoinProtection.resistanceDuration / 2) Ticks.TICKS_PER_SECOND * (JoinProtection.resistanceDuration / 2)
); );
} }

View File

@ -1,6 +1,9 @@
package eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.teams; package eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.teams;
import eu.mhsl.craftattack.spawn.core.Main;
import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo; import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.fightDetector.FightDetector;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -62,4 +65,28 @@ public class VaroTeam {
.filter(Objects::nonNull) .filter(Objects::nonNull)
.forEach(player -> player.kick(disconnectInfo.getComponent())); .forEach(player -> player.kick(disconnectInfo.getComponent()));
} }
public void timeOverKick() {
boolean isInFight = Main.instance().getAppliance(FightDetector.class).isInFight(this);
if(isInFight) {
Main.logger().info(String.format("Cannot kick Team %s because it is in a fight!", this.name));
Bukkit.getScheduler().runTaskLater(
Main.instance(),
this::timeOverKick,
Ticks.TICKS_PER_SECOND * 15
);
return;
}
Main.logger().info(String.format("Kicking Team %s because time is up!", this.name));
DisconnectInfo timeOverInfo = new DisconnectInfo(
"Die Zeit ist abgelaufen!",
"Deine Spielzeit ist vorüber. Falls dir noch weitere Zeit zusteht kannst du jetzt eine Pause machen und anschließend erneut beitreten.",
"Falls du Fragen hast, melde dich bitte bei einem Admin!",
UUID.nameUUIDFromBytes("".getBytes())
);
this.kickTeam(timeOverInfo);
}
} }