Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public static boolean requireProvinces(org.bukkit.command.CommandSender sender)
public static int campaignRaidIntruderDamageIntervalTicks;
public static int campaignRaidIntruderDamageAmount;

public static int battleEmptySideGraceSeconds = 300;
public static boolean battleTimeCapEnabled = false;
public static int battleTimeCapMinutes = 120;
public static int battleProvincePollIntervalTicks;
public static int battleProvinceLeaveCountdownSeconds;
public static boolean battleProvinceBlockProtectionEnabled;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public void loadConfig(File configFile) {
Cache.battleCampaignTemplateRaid = config.getString("battle.campaign_template.raid", "raid_template");
Cache.battleItemDurabilityMultiplier = clampUnit(
config.getDouble("battle.item_durability_multiplier", 0.2));
Cache.battleEmptySideGraceSeconds = Math.max(0, config.getInt("battle.empty_side_grace_seconds", 300));
Cache.battleTimeCapEnabled = config.getBoolean("battle.time_cap_enabled", false);
Cache.battleTimeCapMinutes = config.getInt("battle.time_cap_minutes", 120);
validateBattlePresenceConfig();
validateBattleTemplateDefaultsConfig();

Expand Down Expand Up @@ -217,13 +220,13 @@ public void loadWar(File warFile) {
Cache.warBattleMinSideLives = config.getInt("war.battle_military.min_side_lives", 1);
int deathsPerRegiment = config.getInt("war.battle_military.deaths_per_regiment_loss", 5);
Cache.warBattleDeathsPerRegimentLoss = deathsPerRegiment > 0 ? deathsPerRegiment : 5;
Cache.battleLootMode = BattleLootMode.fromJson(config.getString("war.battle_loot.mode", "COMMAND"));
Cache.battleLootMode = BattleLootMode.fromJson(config.getString("war.battle_loot.mode", "ITEM"));
if (Cache.battleLootMode == null) {
Cache.battleLootMode = BattleLootMode.COMMAND;
}
Cache.battleLootCommands = new ArrayList<>(config.getStringList("war.battle_loot.commands"));
Cache.battleLootItemPath = config.getString("war.battle_loot.item", "");
Cache.battleLootItemAmount = Math.max(1, config.getInt("war.battle_loot.item_amount", 1));
Cache.battleLootItemPath = config.getString("war.battle_loot.item", "m.materials.ignitium");
Cache.battleLootItemAmount = Math.max(1, config.getInt("war.battle_loot.item_amount", 4));
Cache.campaignRaidMusterSeconds = config.getInt("war.campaign_raid.muster_seconds", 60);
Cache.campaignRaidMusterReminderSecondsBefore = loadReminderOffsets(
config,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package net.tfminecraft.simplefactions.war.battle.campaign;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

import net.tfminecraft.simplefactions.utils.Permissions;
import net.tfminecraft.simplefactions.war.core.War;
import net.tfminecraft.simplefactions.war.battle.engine.core.Battle;
import net.tfminecraft.simplefactions.war.battle.engine.core.BattleFactory;
import net.tfminecraft.simplefactions.war.battle.engine.core.BattleManager;
import net.tfminecraft.simplefactions.war.battle.engine.core.BattlePlacementValidator;
import net.tfminecraft.simplefactions.war.battle.persistence.BattlePersistenceService;
import net.tfminecraft.simplefactions.war.battle.enums.BattleType;
import net.tfminecraft.simplefactions.war.battle.campaign.BattleNamingService;
Expand All @@ -18,14 +26,25 @@
import net.tfminecraft.simplefactions.war.campaign.progression.postbattle.CampaignOffensiveForfeitService;
import net.tfminecraft.simplefactions.war.campaign.runtime.BattleScheduleService;
import net.tfminecraft.simplefactions.war.campaign.runtime.BattleSideMembers;
import net.tfminecraft.simplefactions.war.campaign.runtime.BattleWindowService;
import net.tfminecraft.simplefactions.war.campaign.schedule.CampaignScheduleService;
import net.tfminecraft.simplefactions.war.campaign.schedule.ScheduledCampaignBattle;
import net.tfminecraft.simplefactions.SimpleFactions;

public final class CampaignBattleLaunchService {
private static final DateTimeFormatter SCHEDULED_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
.withZone(BattleWindowService.SCHEDULE_ZONE);
private static final Map<String, String> belligerentStartFailure = new ConcurrentHashMap<>();
private static final Map<String, String> adminStartFailure = new ConcurrentHashMap<>();

private CampaignBattleLaunchService() {
}

static void resetStartFailureAlertsForTests() {
belligerentStartFailure.clear();
adminStartFailure.clear();
}

public static Battle prepareScheduledBattle(War war) {
if (war == null || !war.isActive()) {
return null;
Expand All @@ -44,7 +63,9 @@ public static Battle prepareScheduledBattle(War war) {
return null;
}

return createCampaignBattle(war, provinceId, false);
Battle battle = createCampaignBattle(war, provinceId, false);
alertStaffOfScheduledBattle(war, battle);
return battle;
}

public static Battle launchAutoresolveBattle(War war) {
Expand All @@ -61,8 +82,7 @@ public static Battle launchAutoresolveBattle(War war) {
if (existing != null) {
String startError = startPreparedBattle(war, existing);
if (startError != null) {
SimpleFactions.plugin.getLogger().warning(
"[SimpleFactions] Could not start autoresolve battle: " + startError);
logWarning("Could not start autoresolve battle: " + startError);
}
return existing;
}
Expand All @@ -75,8 +95,7 @@ public static Battle launchAutoresolveBattle(War war) {
if (battle != null) {
String startError = startPreparedBattle(war, battle);
if (startError != null) {
SimpleFactions.plugin.getLogger().warning(
"[SimpleFactions] Could not start autoresolve battle: " + startError);
logWarning("Could not start autoresolve battle: " + startError);
}
}
return battle;
Expand Down Expand Up @@ -113,8 +132,7 @@ public static boolean tryStartScheduledBattle(War war, Instant now) {
}
String startError = startPreparedBattle(war, battle);
if (startError != null) {
SimpleFactions.plugin.getLogger().warning(
"[SimpleFactions] Could not start scheduled battle: " + startError);
logWarning("Could not start scheduled battle: " + startError);
broadcastStartFailure(war, battle, startError);
return false;
}
Expand All @@ -139,6 +157,8 @@ private static Battle createCampaignBattle(War war, int provinceId, boolean imme
.orElse(null);
BattleType type = CampaignBattleTypeResolver.resolve(war, slot);
String battleId = campaignBattleId(war.getId(), provinceId);
// A replacement battle reuses the id, so its first failure must alert again.
clearStartFailureAlerts(war, battleId);

Battle battle = BattleFactory.createBlank(type, battleId);
battle.setWarId(war.getId());
Expand All @@ -162,10 +182,90 @@ static String campaignBattleId(int warId, int provinceId) {
return "campaign_w" + warId + "_p" + provinceId;
}

private static void broadcastStartFailure(War war, Battle battle, String error) {
static void broadcastStartFailure(War war, Battle battle, String error) {
String battleName = battle != null ? battle.getDisplayName() : "The scheduled battle";
String message = "§c" + battleName + " could not start: §7" + error;
broadcastToBelligerents(war, message);
String key = failureKey(war, battle);
if (belligerentStartFailure.putIfAbsent(key, error) == null) {
broadcastToBelligerents(war, message);
}
String previousAdmin = adminStartFailure.put(key, error);
if (!error.equals(previousAdmin)) {
sendToOnlineAdmins(message);
}
}

private static void clearStartFailureAlerts(War war, String battleId) {
String key = battleId.toLowerCase(Locale.ROOT);
belligerentStartFailure.remove(key);
adminStartFailure.remove(key);
String warKey = failureKey(war, null);
belligerentStartFailure.remove(warKey);
adminStartFailure.remove(warKey);
}

private static String failureKey(War war, Battle battle) {
if (battle != null && battle.getId() != null && !battle.getId().isBlank()) {
return battle.getId().toLowerCase(Locale.ROOT);
}
return war != null ? "war-" + war.getId() : "unknown";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private static void alertStaffOfScheduledBattle(War war, Battle battle) {
if (battle == null) {
return;
}
List<String> missing = BattlePlacementValidator.validate(battle);
String missingText = missing.isEmpty() ? "none" : String.join("; ", missing);
String when = formatScheduledTime(war != null ? war.getScheduledBattleAt() : null);
String type = scheduledBattleTypeLabel(battle);
String plain = battle.getDisplayName()
+ " (" + type + ") province " + battle.getProvinceId()
+ " at " + when
+ ". Still missing: " + missingText;
logWarning(plain);
sendToOnlineAdmins("§cScheduled battle §e" + battle.getDisplayName()
+ " §7(" + type + ") §cprovince §e" + battle.getProvinceId()
+ " §cat §e" + when
+ "§c. Still missing: §7" + missingText);
}

static String scheduledBattleTypeLabel(Battle battle) {
if (battle != null && battle.isNavalVariant()) {
return "naval";
}
if (battle != null && battle.getBattleType() == BattleType.SIEGE) {
return "siege";
}
return "field";
}

static String formatScheduledTime(Instant scheduledAt) {
if (scheduledAt == null) {
return "unscheduled";
}
return SCHEDULED_TIME.format(scheduledAt) + " CET";
}

private static void logWarning(String plain) {
Logger logger = SimpleFactions.plugin != null ? SimpleFactions.plugin.getLogger() : null;
if (logger == null) {
logger = Bukkit.getLogger();
}
if (logger != null) {
logger.warning("[SimpleFactions] " + plain);
}
}

private static void sendToOnlineAdmins(String message) {
if (Bukkit.getOnlinePlayers() == null) {
return;
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (player != null && player.isOnline() && Permissions.isAdmin(player)) {
player.sendMessage(message);
}
}
}

private static void broadcastToBelligerents(War war, String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ public static CampaignBattleApplyResult applyCampaignBattleOutcome(
sideCasualties != null ? sideCasualties : Map.of());
}

CampaignBattleEndService.spendOffensiveFuel(war);
if (winnerRole != null) {
CampaignBattleEndService.spendOffensiveFuel(war);
}
CampaignBattleEndService.clearHoldPeace(war);

ScheduledCampaignBattle foughtSlot = CampaignScheduleService.slotAtActiveIndex(war).orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import net.tfminecraft.simplefactions.war.battle.engine.raid.BattleRaidSetup;
import net.tfminecraft.simplefactions.war.battle.engine.raid.RaidAttackerEliminationService;
import net.tfminecraft.simplefactions.war.battle.engine.raid.RaidWinService;
import net.tfminecraft.simplefactions.war.battle.engine.win.BattleTimeCap;
import net.tfminecraft.simplefactions.war.battle.engine.win.FieldWinService;
import net.tfminecraft.simplefactions.war.battle.engine.win.SiegeContestService;
import net.tfminecraft.simplefactions.war.battle.engine.win.SiegeWinService;
Expand Down Expand Up @@ -301,7 +302,11 @@ public String start() {
pm.setPoints(this.points);
if(teleport) {
for(Player p : getAllParticipants()) {
p.teleport(getSideByPlayer(p).getSpawn());
BattleSide side = getSideByPlayer(p);
if (side == null || side.getSpawn() == null) {
continue;
}
p.teleport(side.getSpawn());
if(battleType == BattleType.FIELD && pm.getPoints().size() > 0) {
Battle b = this;
new BukkitRunnable()
Expand Down Expand Up @@ -340,6 +345,7 @@ public void end() {
contestHoldRemainingSeconds = 0;
SiegeContestService.clearBattleState(this);
RaidAttackerEliminationService.clearBattleState(this);
FieldWinService.clearEmptySideTracking(this);
BattleCasualtyLedger.clear(this);
pm.end(getAllParticipants());
if (campaignRaid) {
Expand Down Expand Up @@ -398,6 +404,12 @@ public void tick() {
}
}
tickPoints();
if (battleType == BattleType.FIELD || battleType == BattleType.SIEGE) {
BattleTimeCap.check(this);
if (!started) {
return;
}
}
if (battleType == BattleType.FIELD) {
FieldWinService.checkFieldWin(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ public static List<String> validate(Battle battle) {
return Collections.emptyList();
}
List<String> errors = new ArrayList<>();
if (battle.getWarId() != null) {
for (BattleSide side : battle.getSides()) {
if (side == null) {
continue;
}
String label = sideLabel(side);
if (side.getSpawn() == null) {
errors.add(label + " spawn is not set.");
}
if (side.getJail() == null) {
errors.add(label + " jail is not set.");
}
}
}
if (battle.getBattleType() == BattleType.SIEGE) {
ContestArea area = battle.getContestArea();
if (area == null || !area.isConfigured()) {
Expand All @@ -73,6 +87,14 @@ public static List<String> validate(Battle battle) {
return errors;
}

private static String sideLabel(BattleSide side) {
String id = side.getId();
if (id == null || id.isBlank()) {
return "Side";
}
return id.substring(0, 1).toUpperCase() + id.substring(1);
}

public static String validateForStart(Battle battle) {
List<String> errors = validate(battle);
if (errors.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package net.tfminecraft.simplefactions.war.battle.engine.win;

import java.time.Instant;

import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.war.battle.engine.core.Battle;
import net.tfminecraft.simplefactions.war.battle.engine.core.BattleEndSupport;
import net.tfminecraft.simplefactions.war.battle.engine.core.BattleSide;
import net.tfminecraft.simplefactions.war.battle.enums.BattleEndReason;
import net.tfminecraft.simplefactions.war.battle.enums.BattleType;

public final class BattleTimeCap {
private BattleTimeCap() {
}

public static void check(Battle battle) {
check(battle, Instant.now());
}

static void check(Battle battle, Instant now) {
if (battle == null || !battle.hasStarted() || now == null || battle.getWarId() == null) {
return;
}
BattleType type = battle.getBattleType();
if (type != BattleType.FIELD && type != BattleType.SIEGE) {
return;
}
if (!Cache.battleTimeCapEnabled || Cache.battleTimeCapMinutes <= 0 || battle.getStartedAt() == null) {
return;
}
Instant deadline = battle.getStartedAt().plusSeconds(Cache.battleTimeCapMinutes * 60L);
if (now.isBefore(deadline)) {
return;
}
BattleEndSupport.endBattle(battle, winnerByLives(battle), BattleEndReason.TIMER);
}

private static String winnerByLives(Battle battle) {
String winner = null;
int best = Integer.MIN_VALUE;
boolean tie = false;
for (BattleSide side : battle.getSides()) {
if (side == null) {
continue;
}
int lives = side.getLives();
if (winner == null || lives > best) {
winner = side.getId();
best = lives;
tie = false;
} else if (lives == best) {
tie = true;
}
}
return tie ? null : winner;
}
}
Loading