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
4 changes: 3 additions & 1 deletion src/main/java/net/tfminecraft/infestations/Infestations.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ private boolean loadConfigs() {
ok &= configLoader.loadSafe(new File(getDataFolder(), "config.yml"));
Messages.load(new File(getDataFolder(), "messages.yml"));
ok &= groupLoader.loadSafe(new File(getDataFolder(), "groups.yml"));
ok &= net.tfminecraft.infestations.reward.RewardConfig.loadSafe(new File(getDataFolder(), "rewards.yml"));
net.tfminecraft.infestations.spawn.SpawnLog.configure(
Cache.loggingEnabled, Cache.wipeLog, getDataFolder());
if (ok) {
Expand Down Expand Up @@ -127,7 +128,8 @@ private void createConfigs() {
String[] defaultFiles = {
"config.yml",
"messages.yml",
"groups.yml"
"groups.yml",
"rewards.yml"
};
for (String path : defaultFiles) {
copyResourceIfMissing(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public class InfestationData {
public int lureReleased;
public List<String> committed = new ArrayList<>();
public Map<String, Long> logoutGraceUntil = new HashMap<>();
public Map<String, Integer> lureKills = new HashMap<>();
public List<String> deathOnLogin = new ArrayList<>();
public Map<String, String> logoutCharacter = new HashMap<>();
public Map<String, String> joinCharacter = new HashMap<>();
public Map<String, List<String>> owedDeaths = new HashMap<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,24 +121,26 @@ private static Infestation fromData(InfestationData row) {
}
}
}
if (row.deathOnLogin != null) {
for (String id : row.deathOnLogin) {
if (id == null) continue;
if (row.lureKills != null) {
for (Map.Entry<String, Integer> e : row.lureKills.entrySet()) {
if (e.getKey() == null || e.getValue() == null) continue;
try {
infestation.getDeathOnLogin().add(UUID.fromString(id));
infestation.getLureKills().put(UUID.fromString(e.getKey()), e.getValue());
} catch (IllegalArgumentException ignored) {
}
}
}
if (row.logoutCharacter != null) {
for (Map.Entry<String, String> e : row.logoutCharacter.entrySet()) {
if (e.getKey() == null || e.getValue() == null) continue;
if (row.deathOnLogin != null) {
for (String id : row.deathOnLogin) {
if (id == null) continue;
try {
infestation.getLogoutCharacter().put(UUID.fromString(e.getKey()), e.getValue());
infestation.getDeathOnLogin().add(UUID.fromString(id));
} catch (IllegalArgumentException ignored) {
}
}
}
readCharacters(row.logoutCharacter, infestation.getLogoutCharacter());
readCharacters(row.joinCharacter, infestation.getJoinCharacter());
if (row.owedDeaths != null) {
for (Map.Entry<String, List<String>> e : row.owedDeaths.entrySet()) {
if (e.getKey() == null || e.getValue() == null) continue;
Expand All @@ -155,6 +157,19 @@ private static Infestation fromData(InfestationData row) {
return infestation;
}

private static void readCharacters(Map<String, String> source, Map<UUID, String> target) {
if (source == null) {
return;
}
for (Map.Entry<String, String> e : source.entrySet()) {
if (e.getKey() == null || e.getValue() == null) continue;
try {
target.put(UUID.fromString(e.getKey()), e.getValue());
} catch (IllegalArgumentException ignored) {
}
}
}

private static InfestationData toData(Infestation infestation) {
InfestationData row = new InfestationData();
row.provinceId = infestation.getProvinceId();
Expand All @@ -178,12 +193,18 @@ private static InfestationData toData(Infestation infestation) {
for (Map.Entry<UUID, Long> e : infestation.getLogoutGraceUntil().entrySet()) {
row.logoutGraceUntil.put(e.getKey().toString(), e.getValue());
}
for (Map.Entry<UUID, Integer> e : infestation.getLureKills().entrySet()) {
row.lureKills.put(e.getKey().toString(), e.getValue());
}
for (UUID id : infestation.getDeathOnLogin()) {
row.deathOnLogin.add(id.toString());
}
for (Map.Entry<UUID, String> e : infestation.getLogoutCharacter().entrySet()) {
row.logoutCharacter.put(e.getKey().toString(), e.getValue());
}
for (Map.Entry<UUID, String> e : infestation.getJoinCharacter().entrySet()) {
row.joinCharacter.put(e.getKey().toString(), e.getValue());
}
for (Map.Entry<UUID, Set<String>> e : infestation.getOwedDeaths().entrySet()) {
row.owedDeaths.put(e.getKey().toString(), new ArrayList<>(e.getValue()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ public long getLureGeneration() {
private int victoryAtTick;
private final Set<UUID> committed = new HashSet<>();
private final Map<UUID, Long> logoutGraceUntil = new HashMap<>();
private final Map<UUID, Integer> lureKills = new HashMap<>();
private final Set<UUID> deathOnLogin = new HashSet<>();
/** The character each committed player logged off as, so a death on login reaches only that character. */
private final Map<UUID, String> logoutCharacter = new HashMap<>();
/** The character each player committed with, so only that character can claim the lure's rewards. */
private final Map<UUID, String> joinCharacter = new HashMap<>();
/**
* Deaths still owed after their lure ended, by account: the characters that abandoned a running fight,
* or {@link #ACCOUNT} on servers without characters. They outlive lures until settled; clearing the infestation forgives them.
Expand Down Expand Up @@ -133,8 +136,10 @@ public void placeLure(Block block, long joinEndsAt, int budget) {
this.enemiesAlive = 0;
this.committed.clear();
this.logoutGraceUntil.clear();
this.lureKills.clear();
this.deathOnLogin.clear();
this.logoutCharacter.clear();
this.joinCharacter.clear();
this.waveRetryAtTick = 0;
this.lureActivatedAt = 0;
this.lureReleased = 0;
Expand All @@ -154,8 +159,10 @@ public void clearLure() {
enemiesAlive = 0;
committed.clear();
logoutGraceUntil.clear();
lureKills.clear();
deathOnLogin.clear();
logoutCharacter.clear();
joinCharacter.clear();
waveRetryAtTick = 0;
lureActivatedAt = 0;
lureReleased = 0;
Expand Down Expand Up @@ -300,6 +307,18 @@ public Map<UUID, Long> getLogoutGraceUntil() {
return logoutGraceUntil;
}

public Map<UUID, Integer> getLureKills() {
return lureKills;
}

public void addLureKill(UUID id) {
lureKills.merge(id, 1, Integer::sum);
}

public int lureKills(UUID id) {
return lureKills.getOrDefault(id, 0);
}

public Set<UUID> getDeathOnLogin() {
return deathOnLogin;
}
Expand All @@ -308,6 +327,10 @@ public Map<UUID, String> getLogoutCharacter() {
return logoutCharacter;
}

public Map<UUID, String> getJoinCharacter() {
return joinCharacter;
}

public Map<UUID, Set<String>> getOwedDeaths() {
return owedDeaths;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import net.tfminecraft.infestations.lure.LureFurniture;
import net.tfminecraft.infestations.lure.LureHologram;
import net.tfminecraft.infestations.map.InfestationMapExport;
import net.tfminecraft.infestations.reward.RewardDrops;
import net.tfminecraft.infestations.spawn.AmbientSpawnService;
import net.tfminecraft.infestations.spawn.Keys;
import net.tfminecraft.infestations.spawn.LureSpawnQuota;
Expand Down Expand Up @@ -838,6 +839,7 @@ private void victory(Infestation infestation) {
loc.getWorld().spawnParticle(Particle.FIREWORK, loc.clone().add(0, 1, 0), 20, 0.3, 0.5, 0.3, 0.05);
loc.getWorld().playSound(loc, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1.1f);
}
RewardDrops.lureVictory(infestation, loc, rewardable(infestation));
for (Player player : Bukkit.getOnlinePlayers()) {
if (Provinces.within(player, provinceId)) {
player.sendMessage(Messages.get("lure-victory"));
Expand All @@ -848,6 +850,26 @@ private void victory(Infestation infestation) {
InfestationMapExport.exportAsync(byProvince.values());
}

/** Committed players who are online, alive and on the character that joined the lure. */
private List<Player> rewardable(Infestation infestation) {
List<Player> players = new ArrayList<>();
for (UUID id : infestation.getCommitted()) {
Player player = Bukkit.getPlayer(id);
if (player != null && player.isOnline() && !player.isDead()
&& !infestation.getDeathOnLogin().contains(id) && !heldByOtherCharacter(infestation, player)
&& onJoinCharacter(infestation, player)) {
players.add(player);
}
}
return players;
}

/** Lures saved before join characters were recorded have none, so any character passes. */
private static boolean onJoinCharacter(Infestation infestation, Player player) {
String joined = infestation.getJoinCharacter().get(player.getUniqueId());
return joined == null || joined.equalsIgnoreCase(Characters.activeId(player));
}

private void destroyLure(Infestation infestation, boolean victory) {
LureHologram.remove(infestation);
pluginFurnitureRemove = true;
Expand Down Expand Up @@ -927,6 +949,17 @@ private void tryJoin(Player player, Infestation infestation) {
infestation.getLogoutGraceUntil().remove(player.getUniqueId());
infestation.getDeathOnLogin().remove(player.getUniqueId());
infestation.getLogoutCharacter().remove(player.getUniqueId());
String character = Characters.activeId(player);
String previous = infestation.getJoinCharacter().get(player.getUniqueId());
// Lure kills are counted per account, so a different character starts from zero.
if (previous != null && character != null && !previous.equalsIgnoreCase(character)) {
infestation.getLureKills().remove(player.getUniqueId());
}
if (character != null) {
infestation.getJoinCharacter().put(player.getUniqueId(), character);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
} else {
infestation.getJoinCharacter().remove(player.getUniqueId());
}
player.sendMessage(Messages.get("lure-join"));
save();
}
Expand Down Expand Up @@ -1140,17 +1173,24 @@ public void onDeath(EntityDeathEvent event) {
return;
}
recentDeaths.put(entity.getUniqueId(), new RecentDeath(provinceId, kind, tick));
Player killer = entity.getKiller();
if (Keys.KIND_AMBIENT.equals(kind)) {
if (infestation.getAmbientAlive() > 0) {
infestation.setAmbientAlive(infestation.getAmbientAlive() - 1);
}
if (killer != null && !Keys.isSummoned(entity.getPersistentDataContainer())) {
RewardDrops.ambientKill(infestation, killer, entity.getLocation());
}
return;
}
if (Keys.KIND_LURE.equals(kind) && infestation.getPhase() == LurePhase.ACTIVE) {
if (infestation.getEnemiesAlive() > 0) {
infestation.setEnemiesAlive(infestation.getEnemiesAlive() - 1);
}
infestation.setLureRemaining(infestation.getLureRemaining() - 1);
if (killer != null) {
infestation.addLureKill(killer.getUniqueId());
}
}
}

Expand Down Expand Up @@ -1183,11 +1223,13 @@ private void adoptSummon(ActiveMob mob) {
}
if (infestation.getPhase() == LurePhase.ACTIVE && Keys.KIND_LURE.equals(death.kind())) {
Keys.tagMob(living.getPersistentDataContainer(), death.provinceId(), Keys.KIND_LURE);
Keys.markSummoned(living.getPersistentDataContainer());
infestation.setEnemiesAlive(infestation.getEnemiesAlive() + 1);
infestation.setLureRemaining(infestation.getLureRemaining() + 1);
SpawnLog.line(infestation, "-", "summoned remaining=" + infestation.getLureRemaining());
} else if (infestation.getPhase() == LurePhase.NONE && Keys.KIND_AMBIENT.equals(death.kind())) {
Keys.tagMob(living.getPersistentDataContainer(), death.provinceId(), Keys.KIND_AMBIENT);
Keys.markSummoned(living.getPersistentDataContainer());
infestation.setAmbientAlive(infestation.getAmbientAlive() + 1);
}
}
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/net/tfminecraft/infestations/reward/KeyCap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.tfminecraft.infestations.reward;

import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
* Counts ambient key drops per player over a rolling hour.
*/
final class KeyCap {

static final long WINDOW_MS = 3_600_000L;

private final Map<UUID, ArrayDeque<Long>> given = new HashMap<>();
private long lastSweep;

/** How many of {@code wanted} keys the player may still get. A cap of 0 or less means no cap. */
int allow(UUID player, int wanted, int cap, long now) {
if (wanted <= 0) {
return 0;
}
if (cap <= 0) {
return wanted;
}
sweep(now);
ArrayDeque<Long> times = given.computeIfAbsent(player, k -> new ArrayDeque<>());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
expire(times, now);
int allowed = Math.min(wanted, cap - times.size());
for (int i = 0; i < allowed; i++) {
times.addLast(now);
}
return Math.max(0, allowed);
}

/** Forgets players whose drops have all left the window, at most once per window. */
private void sweep(long now) {
if (now - lastSweep < WINDOW_MS) {
return;
}
lastSweep = now;
given.values().removeIf(times -> {
expire(times, now);
return times.isEmpty();
});
}

private static void expire(ArrayDeque<Long> times, long now) {
while (!times.isEmpty() && times.peekFirst() <= now - WINDOW_MS) {
times.pollFirst();
}
}

int tracked() {
return given.size();
}

void clear() {
given.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.tfminecraft.infestations.reward;

import java.lang.reflect.Method;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.plugin.Plugin;

import net.tfminecraft.infestations.Infestations;

/**
* Asks RPCharacters whether a location is inside a permadeath zone. Uses reflection so Infestations
* never depends on RPCharacters. Without RPCharacters, or on any error, the location is outside a zone.
*/
public final class PermadeathZones {

static final String LOOKUP = "net.tfminecraft.rpcharacters.permadeath.PermadeathAreaLookup";

private static ClassLoader loader;
private static Method lookup;
private static boolean warned;

private PermadeathZones() {}

public static boolean inZone(Location location) {
if (location == null) {
return false;
}
Plugin plugin = Bukkit.getPluginManager().getPlugin("RPCharacters");
if (plugin == null || !plugin.isEnabled()) {
return false;
}
try {
return method(plugin.getClass().getClassLoader()).invoke(null, location) != null;
} catch (ReflectiveOperationException | RuntimeException | LinkageError ex) {
if (!warned) {
warned = true;
Infestations.plugin.getLogger().warning(
"[Infestations] Permadeath zone lookup failed, treating locations as outside zones: " + ex);
}
return false;
}
}

static Method method(ClassLoader classLoader) throws ReflectiveOperationException {
if (lookup == null || loader != classLoader) {
lookup = Class.forName(LOOKUP, true, classLoader).getMethod("getPermadeathZoneAt", Location.class);
loader = classLoader;
}
return lookup;
}
}
Loading