Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db35c28f0b |
No files matched your search
@@ -45,6 +45,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
private games.dmg.fantasystick.session.SessionVitals vitals;
|
||||
private games.dmg.fantasystick.session.SessionVisuals sessionVisuals;
|
||||
private games.dmg.fantasystick.combat.BasicAttack basicAttack;
|
||||
private games.dmg.fantasystick.session.NutritionPolicy nutritionPolicy;
|
||||
private java.util.concurrent.CompletionStage<Void> configurationDrain = java.util.concurrent.CompletableFuture.completedStage(null);
|
||||
|
||||
@Override public void onEnable() {
|
||||
@@ -109,6 +110,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
notice -> notifyPlayer(generation, notice), diagnostics, presentation, adventureSettings::enabled, gameModes);
|
||||
vitals = new games.dmg.fantasystick.session.SessionVitals();
|
||||
sessions.attachVitals(vitals);
|
||||
nutritionPolicy = new games.dmg.fantasystick.session.NutritionPolicy(sessions);
|
||||
parties = new games.dmg.fantasystick.party.PartyRoster(Clock.systemUTC(), id -> {
|
||||
var player = getServer().getPlayer(id);
|
||||
return player != null && player.isOnline();
|
||||
@@ -143,6 +145,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
getServer().getPluginManager().registerEvents(partyCombat, this);
|
||||
partyCombat.install(this);
|
||||
getServer().getPluginManager().registerEvents(localBasicAttack, this);
|
||||
getServer().getPluginManager().registerEvents(nutritionPolicy, this);
|
||||
getServer().getPluginManager().registerEvents(books, this);
|
||||
getServer().getPluginManager().registerEvents(gameModes, this);
|
||||
sessionVisuals = new games.dmg.fantasystick.session.SessionVisuals(sessions,
|
||||
@@ -181,6 +184,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
var closingVitals = vitals; vitals = null;
|
||||
var closingVisuals = sessionVisuals; sessionVisuals = null;
|
||||
var closingBasicAttack = basicAttack; basicAttack = null;
|
||||
var closingNutrition = nutritionPolicy; nutritionPolicy = null;
|
||||
// Retire client resources independently of successful ordinary-equipment restoration.
|
||||
if (closingPresentation != null) cleanup("presentation", closingPresentation::close);
|
||||
if (closingProtocol != null) cleanup("protocol presentation", closingProtocol::close);
|
||||
@@ -191,6 +195,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
}
|
||||
if (closingBooks != null) cleanup("character book", closingBooks::close);
|
||||
if (closingBasicAttack != null) cleanup("basic attack", closingBasicAttack::close);
|
||||
if (closingNutrition != null) cleanup("nutrition policy", closingNutrition::close);
|
||||
if (closingVisuals != null) cleanup("session particles", closingVisuals::close);
|
||||
if (closingParties != null) cleanup("parties", closingParties::close);
|
||||
if (closingPartyCombat != null) cleanup("party combat", closingPartyCombat::close);
|
||||
@@ -203,6 +208,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
unregister(closingPartyEvents);
|
||||
unregister(closingPartyCombat);
|
||||
unregister(closingBasicAttack);
|
||||
unregister(closingNutrition);
|
||||
unregister(closingBooks);
|
||||
unregister(closingEvents);
|
||||
unregister(closingCrafting);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package games.dmg.fantasystick.session;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
/** Active-session-only hunger/exhaustion suppression; explicit non-food healing remains native. */
|
||||
public final class NutritionPolicy implements Listener, AutoCloseable {
|
||||
private final EquipmentSessions sessions;
|
||||
private boolean closed;
|
||||
|
||||
public NutritionPolicy(EquipmentSessions sessions) {
|
||||
this.sessions = Objects.requireNonNull(sessions);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void food(org.bukkit.event.entity.FoodLevelChangeEvent event) {
|
||||
if (!active(event.getEntity())) return;
|
||||
event.setCancelled(true);
|
||||
var player = (Player) event.getEntity();
|
||||
player.setFoodLevel(20);
|
||||
player.setExhaustion(0);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void exhaustion(org.bukkit.event.entity.EntityExhaustionEvent event) {
|
||||
if (!active(event.getEntity())) return;
|
||||
event.setCancelled(true);
|
||||
var player = (Player) event.getEntity();
|
||||
player.setFoodLevel(20);
|
||||
player.setExhaustion(0);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void healing(org.bukkit.event.entity.EntityRegainHealthEvent event) {
|
||||
if (event.getRegainReason() == org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED
|
||||
&& active(event.getEntity())) event.setCancelled(true);
|
||||
}
|
||||
|
||||
private boolean active(org.bukkit.entity.Entity entity) {
|
||||
return !closed && entity instanceof Player player
|
||||
&& sessions.active(player.getUniqueId()).isPresent();
|
||||
}
|
||||
|
||||
@Override public void close() { closed = true; }
|
||||
}
|
||||
@@ -57,10 +57,15 @@ public final class SessionVitals implements AutoCloseable {
|
||||
if (closed) throw new IllegalStateException("Vitals lifetime ended during entry");
|
||||
player.setHealthScale(maximum);
|
||||
if (closed) throw new IllegalStateException("Vitals lifetime ended during entry");
|
||||
player.setFoodLevel(20);
|
||||
player.setExhaustion(0);
|
||||
if (closed) throw new IllegalStateException("Vitals lifetime ended during entry");
|
||||
write(player, new Mark(session, Phase.APPLIED, maximum, ordinary));
|
||||
} catch (RuntimeException | LinkageError failure) {
|
||||
try { releaseMaximum(player, session); }
|
||||
catch (RuntimeException | LinkageError cleanup) { failure.addSuppressed(cleanup); }
|
||||
try { restoreNutrition(player, ordinary); }
|
||||
catch (RuntimeException | LinkageError cleanup) { failure.addSuppressed(cleanup); }
|
||||
try { restoreScale(player, maximum, ordinary); }
|
||||
catch (RuntimeException | LinkageError cleanup) { failure.addSuppressed(cleanup); }
|
||||
// Leave APPLYING proof and the acknowledged receipt: an uncertain health setter
|
||||
@@ -121,7 +126,7 @@ public final class SessionVitals implements AutoCloseable {
|
||||
// completion). Restore positive health first: a zero-health packet opens a client death
|
||||
// screen even when Paper cancels the death and the server-side player survives.
|
||||
player.setHealth(Math.min(ordinary.health(), Objects.requireNonNull(player.getAttribute(Attribute.MAX_HEALTH)).getValue()));
|
||||
player.setFoodLevel(ordinary.food()); player.setSaturation(ordinary.saturation()); player.setExhaustion(ordinary.exhaustion());
|
||||
restoreNutrition(player, ordinary);
|
||||
restoreScale(player, mark.maximum(), ordinary);
|
||||
write(player, new Mark(session, Phase.RELEASED, mark.maximum(), ordinary));
|
||||
return true;
|
||||
@@ -135,6 +140,12 @@ public final class SessionVitals implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreNutrition(Player player, OrdinaryVitals ordinary) {
|
||||
player.setFoodLevel(ordinary.food());
|
||||
player.setSaturation(ordinary.saturation());
|
||||
player.setExhaustion(ordinary.exhaustion());
|
||||
}
|
||||
|
||||
private static void restoreScale(Player player, double maximum, OrdinaryVitals ordinary) {
|
||||
if (player.isHealthScaled() && player.getHealthScale() == maximum) {
|
||||
player.setHealthScale(ordinary.scale());
|
||||
|
||||
@@ -65,6 +65,8 @@ class FantasyStickLifecycleNativeTest {
|
||||
assertTrue(fixture.order.indexOf("PartyCombat") < recipe);
|
||||
assertTrue(fixture.order.indexOf("BasicAttack") >= 0);
|
||||
assertTrue(fixture.order.indexOf("BasicAttack") < recipe);
|
||||
assertTrue(fixture.order.indexOf("NutritionPolicy") >= 0);
|
||||
assertTrue(fixture.order.indexOf("NutritionPolicy") < recipe);
|
||||
assertNotNull(fixture.command);
|
||||
assertEquals("EquipmentAdminCommand", fixture.command.getExecutor().getClass().getSimpleName());
|
||||
assertTrue(fixture.tasks.isEmpty());
|
||||
@@ -1260,6 +1262,7 @@ class FantasyStickLifecycleNativeTest {
|
||||
boolean healthScaled;
|
||||
double healthScale = 20;
|
||||
Runnable scaleMutation = () -> {};
|
||||
Runnable foodMutation = () -> {};
|
||||
java.util.function.Consumer<org.bukkit.event.player.PlayerGameModeChangeEvent> modeEvents = ignored -> { };
|
||||
final org.bukkit.craftbukkit.entity.CraftPlayer player;
|
||||
final net.minecraft.server.level.ServerPlayer nativePlayer = mock(net.minecraft.server.level.ServerPlayer.class);
|
||||
@@ -1293,7 +1296,7 @@ class FantasyStickLifecycleNativeTest {
|
||||
}
|
||||
case "getMaxHealth" -> maximumHealth.getValue();
|
||||
case "getFoodLevel" -> food.getFoodLevel();
|
||||
case "setFoodLevel" -> { food.setFoodLevel((Integer) args[0]); yield null; }
|
||||
case "setFoodLevel" -> { food.setFoodLevel((Integer) args[0]); foodMutation.run(); yield null; }
|
||||
case "getSaturation" -> food.getSaturationLevel();
|
||||
case "setSaturation" -> { food.setSaturation((Float) args[0]); yield null; }
|
||||
case "getExhaustion" -> food.exhaustionLevel;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class NutritionNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void exitEatingAndReentryCannotHealTheSavedCharacterOrLoseTheNewOrdinaryNutrition() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"), "character-swap-cooldown-seconds: 0\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(a.core.id, a.player);
|
||||
a.player.setHealth(12); a.player.setFoodLevel(7); a.player.setSaturation(2); a.player.setExhaustion(3);
|
||||
f.activate(a); a.player.setHealth(4);
|
||||
var sheet = a.core.inventory.getItemInOffHand().clone();
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a); f.awaitCharacterAndReceipt(a);
|
||||
assertEquals(12, a.player.getHealth()); assertEquals(7, a.player.getFoodLevel());
|
||||
a.player.setHealth(a.maximumHealth.getValue());
|
||||
a.player.setFoodLevel(20); a.player.setSaturation(12); a.player.setExhaustion(0.5f);
|
||||
a.core.inventory.setItemInOffHand(sheet); f.activate(a);
|
||||
assertEquals(4, a.player.getHealth(), "Ordinary eating/healing cannot refill transferable fantasy health");
|
||||
assertEquals(20, a.player.getFoodLevel()); assertEquals(0, a.player.getExhaustion());
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a); f.awaitCharacterAndReceipt(a);
|
||||
assertEquals(a.maximumHealth.getValue(), a.player.getHealth());
|
||||
assertEquals(20, a.player.getFoodLevel()); assertEquals(12, a.player.getSaturation());
|
||||
assertEquals(0.5f, a.player.getExhaustion());
|
||||
var characters = (games.dmg.fantasystick.persistence.CharacterStorage)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
assertEquals(0, characters.find(a.core.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.orElseThrow().health().compareTo(java.math.BigDecimal.valueOf(4)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void failedFantasyFoodSetterRestoresTheCapturedOrdinaryNutritionAndRetainsRecoveryProof() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(a.core.id, a.player);
|
||||
a.player.setFoodLevel(7); a.player.setSaturation(2); a.player.setExhaustion(3);
|
||||
a.foodMutation = () -> {
|
||||
a.foodMutation = () -> {};
|
||||
throw new IllegalStateException("Injected food setter failure");
|
||||
};
|
||||
f.activate(a);
|
||||
var sessions = (games.dmg.fantasystick.session.EquipmentSessions)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "sessions");
|
||||
assertTrue(sessions.active(a.core.id).isEmpty());
|
||||
assertEquals(7, a.player.getFoodLevel()); assertEquals(2, a.player.getSaturation());
|
||||
assertEquals(3, a.player.getExhaustion());
|
||||
assertNotNull(f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.orElseThrow().pending());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void fantasyDefeatRestoresOrdinaryNutritionInsteadOfLeavingTheFullFantasyState() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(a.core.id, a.player);
|
||||
a.player.setFoodLevel(7); a.player.setSaturation(2); a.player.setExhaustion(3);
|
||||
f.activate(a);
|
||||
assertEquals(20, a.player.getFoodLevel()); assertEquals(0, a.player.getExhaustion());
|
||||
var source = new org.bukkit.craftbukkit.damage.CraftDamageSource(new net.minecraft.world.damagesource.DamageSource(
|
||||
org.bukkit.craftbukkit.damage.CraftDamageType.bukkitToMinecraftHolder(org.bukkit.damage.DamageType.OUT_OF_WORLD)));
|
||||
var death = new org.bukkit.event.entity.PlayerDeathEvent(a.player, source, new java.util.ArrayList<>(), 0,
|
||||
net.kyori.adventure.text.Component.empty(), true);
|
||||
f.dispatcher.accept(death); PartyBookNativeTest.drain(f);
|
||||
assertTrue(death.isCancelled());
|
||||
assertEquals(7, a.player.getFoodLevel()); assertEquals(2, a.player.getSaturation());
|
||||
assertEquals(3, a.player.getExhaustion());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void activeFantasyModeKeepsFoodFullBlocksFoodHealingAndRestoresOrdinaryNutrition() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var ordinary = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
ordinary.core.inventory.setItemInOffHand(null);
|
||||
f.players.put(a.core.id, a.player); f.players.put(ordinary.core.id, ordinary.player);
|
||||
a.player.setFoodLevel(7); a.player.setSaturation(2); a.player.setExhaustion(3);
|
||||
ordinary.player.setFoodLevel(8); ordinary.player.setExhaustion(4);
|
||||
f.activate(a);
|
||||
assertEquals(20, a.player.getFoodLevel()); assertEquals(0, a.player.getExhaustion());
|
||||
|
||||
var drain = new org.bukkit.event.entity.FoodLevelChangeEvent(a.player, 19);
|
||||
var exhaustion = new org.bukkit.event.entity.EntityExhaustionEvent(a.player,
|
||||
org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.SPRINT, 0.1f);
|
||||
var foodHealing = new org.bukkit.event.entity.EntityRegainHealthEvent(a.player, 1,
|
||||
org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED);
|
||||
var potionHealing = new org.bukkit.event.entity.EntityRegainHealthEvent(a.player, 1,
|
||||
org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.MAGIC);
|
||||
var abilityHealing = new org.bukkit.event.entity.EntityRegainHealthEvent(a.player, 1,
|
||||
org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.CUSTOM);
|
||||
f.dispatcher.accept(drain); f.dispatcher.accept(exhaustion);
|
||||
f.dispatcher.accept(foodHealing); f.dispatcher.accept(potionHealing); f.dispatcher.accept(abilityHealing);
|
||||
assertTrue(drain.isCancelled()); assertTrue(exhaustion.isCancelled()); assertTrue(foodHealing.isCancelled());
|
||||
assertFalse(potionHealing.isCancelled(), "Explicit native magic healing remains available");
|
||||
assertFalse(abilityHealing.isCancelled(), "Explicit ability healing remains available");
|
||||
assertEquals(20, a.player.getFoodLevel()); assertEquals(0, a.player.getExhaustion());
|
||||
|
||||
var ordinaryDrain = new org.bukkit.event.entity.FoodLevelChangeEvent(ordinary.player, 7);
|
||||
var ordinaryHealing = new org.bukkit.event.entity.EntityRegainHealthEvent(ordinary.player, 1,
|
||||
org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED);
|
||||
f.dispatcher.accept(ordinaryDrain); f.dispatcher.accept(ordinaryHealing);
|
||||
assertFalse(ordinaryDrain.isCancelled()); assertFalse(ordinaryHealing.isCancelled());
|
||||
assertEquals(8, ordinary.player.getFoodLevel()); assertEquals(4, ordinary.player.getExhaustion());
|
||||
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a); f.awaitCharacterAndReceipt(a);
|
||||
assertEquals(7, a.player.getFoodLevel()); assertEquals(2, a.player.getSaturation());
|
||||
assertEquals(3, a.player.getExhaustion());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user