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
13 changes: 13 additions & 0 deletions src/main/java/net/tfminecraft/cooking/Cooking.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package net.tfminecraft.cooking;

import java.io.File;
import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;

import net.tfminecraft.tlibs.TLibs;
Expand Down Expand Up @@ -167,6 +169,7 @@ public void onEnable() {
butterChurnHandler.resumeLoadedChurns();
liquidContainerHandler.resumeLoadedContainers();
meatHookHandler.resumeLoadedHooks();
warnIfCharacterIdentityMissing(getLogger());
SaturationGuard.start();
NutritionDrainTask.start();
Bukkit.getOnlinePlayers().forEach(NutritionLifecycleListener::initializeCurrent);
Expand All @@ -187,6 +190,16 @@ public void onEnable() {
}
}

/** Runs once every plugin has enabled, when RPCharacters 2.16.0+ has registered its provider. */
static void warnIfCharacterIdentityMissing(Logger logger) {
Plugin characters = Bukkit.getPluginManager().getPlugin("RPCharacters");
if (characters == null || !characters.isEnabled() || Characters.available()) return;
logger.warning("RPCharacters " + characters.getPluginMeta().getVersion()
+ " provides no character identity; Cooking needs RPCharacters 2.16.0 or newer."
+ " Until then food and drink give no nutrition, hunger does not drain and the"
+ " hunger bar stays frozen.");
}

@Override
public void onDisable() {
if (incompatibleCharacterApi) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void onMilkBucket(PlayerItemConsumeEvent event) {
event.setCancelled(true);

Player player = event.getPlayer();
PlayerActionContext context = PlayerActionContext.capture(player);
PlayerActionContext context = PlayerActionContext.captureOrRefuse(player);
if (context == null) {
return;
}
Expand All @@ -48,6 +48,18 @@ public void onMilkBucket(PlayerItemConsumeEvent event) {
}
}

/** Keeps the cup when there is no character to drink it, like refused food. */
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onCupWithoutCharacter(PlayerItemConsumeEvent event) {
ItemStack consumed = event.getItem();
if (consumed == null || !ItemCache.isCupOfWater(consumed) && !ItemCache.isCupOfMilk(consumed)) {
return;
}
if (PlayerActionContext.captureOrRefuse(event.getPlayer()) == null) {
event.setCancelled(true);
}
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDrink(PlayerItemConsumeEvent event) {
ItemStack consumed = event.getItem();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static void harvestArea(
if (player == null || tool == null || hoe == null || centre == null) {
return;
}
PlayerActionContext context = PlayerActionContext.capture(player);
PlayerActionContext context = PlayerActionContext.captureOrRefuse(player);
if (context == null) {
return;
}
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/net/tfminecraft/cooking/manager/CommandManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,17 @@ private boolean handleFood(CommandSender sender, String[] args) {
+ " mappedAfter=" + NutritionDisplayService.toFoodLevel(character.getFoodValue())
+ " hudBefore=" + target.getFoodLevel()
+ " saturation=" + target.getSaturation());
if (!context.save()) return true;
if (!context.save()) {
sender.sendMessage("搂cCould not confirm " + target.getName()
+ "'s food was saved: they switched character.");
return true;
}
NutritionDisplayService.sync(context, "admin");
if (!context.isCurrent()) return true;
if (!context.isCurrent()) {
sender.sendMessage("搂c" + target.getName()
+ "'s food was saved, but they switched character before their hunger bar updated.");
return true;
}
NutritionLog.append("SAVE", target, character,
"reason=admin sender=" + sender.getName());
sender.sendMessage("搂aSet " + target.getName() + "'s food from " + before + " to "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void onInteract(FurnitureInteractEvent event) {
player.sendMessage("搂cThis needs to be prepared before you can eat it.");
return;
}
PlayerActionContext context = PlayerActionContext.capture(player);
PlayerActionContext context = PlayerActionContext.captureOrRefuse(player);
if (context == null) return;
food.updateAge();
if (!context.isCurrent()) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void onConsume(PlayerItemConsumeEvent event) {
return;
}

PlayerActionContext context = PlayerActionContext.capture(event.getPlayer());
PlayerActionContext context = PlayerActionContext.captureOrRefuse(event.getPlayer());
if (context == null) {
event.setCancelled(true);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,17 @@ public static void apply(NutritionContext context) {
for (int i = 0; i < resources.length; i++) {
before[i] = resources[i].getCurrent(data);
}
if (!context.isCurrent()) return;
// setBase and setCurrent run MMOCore stat updates and resource events, which other plugins see.
instance.setBase(diet);
if (!context.isCurrent()) return;
double healthCap = Math.min(health, player.getAttribute(Attribute.MAX_HEALTH).getValue());
if (player.getHealth() < healthCap) {
if (!context.isCurrent()) return;
player.setHealth(healthCap);
}
for (int i = 0; i < resources.length; i++) {
if (!context.isCurrent()) return;
double cap = Math.min(before[i], resources[i].getMax(data));
if (resources[i].getCurrent(data) < cap) {
if (!context.isCurrent()) return;
resources[i].setCurrent(data, cap, ResourceUpdateReason.CLAMPING);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,10 @@ public static void sync(NutritionContext context, String reason) {
int foodBefore = player.getFoodLevel();
float saturationBefore = player.getSaturation();
boolean wroteFood = foodBefore != mapped;
if (!context.isCurrent()) return;
if (wroteFood) {
player.setFoodLevel(mapped);
}
if (!context.isCurrent()) return;
player.setSaturation(0f);
if (!context.isCurrent()) return;
NutritionLog.append("SYNC", player, character,
"reason=" + reason
+ " mapped=" + mapped
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package net.tfminecraft.cooking.nutrition;

import java.util.HashMap;
import java.util.Map;

import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
Expand All @@ -12,6 +15,8 @@

public final class NutritionLifecycleListener implements Listener {

private final Map<Player, NutritionContext> dying = new HashMap<>();

@EventHandler(priority = EventPriority.MONITOR)
public void onCharacterStart(CharacterSessionStartEvent event) {
NutritionContext context = NutritionContext.capture(event.getPlayer());
Expand Down Expand Up @@ -45,17 +50,28 @@ public void onPlayerRespawn(PlayerRespawnEvent event) {
NutritionDisplayService.syncFromPlayer(player, "respawn");
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
/** A permakill switches to the replacement at MONITOR, so remember who actually died. */
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void captureDeath(PlayerDeathEvent event) {
NutritionContext context = NutritionContext.capture(event.getEntity());
if (context != null) dying.put(event.getEntity(), context);
}

@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getEntity();
NutritionContext context = NutritionContext.capture(player);
NutritionContext context = dying.remove(player);
if (event.isCancelled()) return;
if (context == null) {
NutritionLog.append("DEATH", player, null, "action=skip cause=no-active-character");
return;
}
RPCharacter character = context.character();
boolean inBattle = BattleFoodGate.inStartedBattle(player);
if (!context.isCurrent()) return;
if (!context.isCurrent()) {
NutritionLog.append("DEATH", player, character, "action=skip cause=character-ended");
return;
}
if (inBattle) {
NutritionLog.append("DEATH", player, character,
"hud=" + player.getFoodLevel() + " saturation=" + player.getSaturation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.player.PlayerJoinEvent;

import net.tfminecraft.rpcharacters.objects.RPCharacter;
import net.tfminecraft.cooking.utils.PlayerActionContext;
Expand Down Expand Up @@ -41,16 +40,6 @@ public static void stop() {
taskId = -1;
}

@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (PlayerActionContext.capture(player) == null) return;
float before = player.getSaturation();
player.setSaturation(0f);
NutritionLog.append("SATURATION", player, null,
"reason=join before=" + before + " after=" + player.getSaturation());
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onFoodLevelChange(FoodLevelChangeEvent event) {
if (!(event.getEntity() instanceof Player player)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void onInteract(FurnitureInteractEvent event) {
}

Player player = event.getPlayer();
PlayerActionContext context = PlayerActionContext.capture(player);
PlayerActionContext context = PlayerActionContext.captureOrRefuse(player);
if (context == null) {
event.setCancelled(true);
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
package net.tfminecraft.cooking.utils;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import net.tfminecraft.tlibs.character.CharacterSession;
import net.tfminecraft.tlibs.character.Characters;

/** One player action, including standalone play without a character provider. */
public record PlayerActionContext(Player player, CharacterSession session) {
public static final String NO_CHARACTER_MESSAGE = "搂cYou need an active character to do that.";
private static final int REFUSAL_COOLDOWN_TICKS = 60;
private static final Map<UUID, Refusal> lastRefusal = new HashMap<>();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/** Per connection, so a rejoin is told again; weak so it never holds an offline player. */
private record Refusal(WeakReference<Player> player, int tick) {}

public static PlayerActionContext capture(Player player) {
if (player == null) return null;
CharacterSession session = Characters.currentSession(player).orElse(null);
Expand All @@ -15,6 +27,22 @@ public static PlayerActionContext capture(Player player) {
return context.isCurrent() ? context : null;
}

/** Captures for an action the player started, telling them (at most every 3 s) when it is refused. */
public static PlayerActionContext captureOrRefuse(Player player) {
PlayerActionContext context = capture(player);
if (context == null && player != null) {
int now = Bukkit.getCurrentTick();
Refusal last = lastRefusal.get(player.getUniqueId());
if (last == null || last.player().get() != player || now - last.tick() >= REFUSAL_COOLDOWN_TICKS) {
// Only refusals still inside the cooldown matter, so the map holds at most a few seconds' worth.
lastRefusal.values().removeIf(old -> now - old.tick() >= REFUSAL_COOLDOWN_TICKS);
lastRefusal.put(player.getUniqueId(), new Refusal(new WeakReference<>(player), now));
player.sendMessage(NO_CHARACTER_MESSAGE);
}
}
return context;
}

public boolean isCurrent() {
return player.isOnline() && Bukkit.getPlayer(player.getUniqueId()) == player
&& (session == null ? !Characters.available()
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/net/tfminecraft/cooking/CookingLifecycleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import net.tfminecraft.cooking.oven.*;
import net.tfminecraft.cooking.sausagemaker.SausageMakerHandler;
import net.tfminecraft.cooking.trough.*;
import net.tfminecraft.cooking.testsupport.TestCharacters;
import net.tfminecraft.tlibs.TLibs;
import net.tfminecraft.tlibs.itemscan.ItemScanService;
import net.tfminecraft.tlibs.objects.api.ItemAPI;
Expand Down Expand Up @@ -154,6 +155,26 @@ class CookingLifecycleTest {
verify(built(PlateManager.class), never()).start();
}

@Test void startupWarnsWhenAnEnabledRPCharactersRegistersNoCharacterIdentity() {
java.util.logging.Logger logger = mock(java.util.logging.Logger.class);
Cooking.warnIfCharacterIdentityMissing(logger);
var characters = MockBukkit.createMockPlugin("RPCharacters");
server.getPluginManager().disablePlugin(characters);
Cooking.warnIfCharacterIdentityMissing(logger);
verifyNoInteractions(logger);

server.getPluginManager().enablePlugin(characters);
Cooking.warnIfCharacterIdentityMissing(logger);
verify(logger).warning(argThat((String message) -> message.startsWith("RPCharacters "
+ characters.getPluginMeta().getVersion() + " provides no character identity")
&& message.contains("RPCharacters 2.16.0 or newer") && message.contains("hunger bar stays frozen")));

try (TestCharacters identities = new TestCharacters(characters)) {
Cooking.warnIfCharacterIdentityMissing(logger);
}
verifyNoMoreInteractions(logger);
}

@Test void nextTickResumesLoadedStateAndShutdownReleasesScansTasksDatabaseAndPathHandler() {
ItemScanService scan = mock(ItemScanService.class);
scans.when(ItemScanService::get).thenReturn(scan);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package net.tfminecraft.cooking.farming;

import net.tfminecraft.cooking.utils.PlayerActionContext;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
Expand Down Expand Up @@ -140,6 +142,7 @@
f.on(BlockBreakEvent.class, event -> breaks.incrementAndGet());
FarmHarvestService.harvestArea(f.player, f.tool(), hoe(), crop);
assertEquals(Material.WHEAT, crop.getType());
assertEquals(PlayerActionContext.NO_CHARACTER_MESSAGE, f.player.nextMessage());
assertEquals(0, breaks.get());
assertEquals(0, ((Damageable) f.tool().getItemMeta()).getDamage());
assertEquals(0, f.inventoryCount(Material.WHEAT));
Expand Down Expand Up @@ -499,7 +502,7 @@
public YamlConfiguration config=new YamlConfiguration(); public final ItemAPI items; private final MockedStatic<TLibs> tlibs; private final StaticConfigSnapshot settings;
private final List<FoodItem> previousFoods; private final List<FoodModel> previousModels; private final Map<String,String> previousConversions;
public Plot(Path directory)throws Exception{
this.directory=directory; server=MockBukkit.mock(); world=spy(new FieldWorld()); world.setName("field"); server.addWorld(world); player=spy(new PlayerMock(server,"Farmer")); server.addPlayer(player); player.teleport(new Location(world,0,65,0)); previous=Cooking.plugin; plugin=mock(Cooking.class); when(plugin.getName()).thenReturn("Cooking"); when(plugin.namespace()).thenReturn("cooking"); when(plugin.isEnabled()).thenReturn(true); when(plugin.getServer()).thenReturn(server); when(plugin.getLogger()).thenReturn(Logger.getLogger("Cooking-test")); when(plugin.getPluginLoader()).thenReturn(MockBukkit.createMockPlugin("CookingFixture").getPluginLoader()); Cooking.plugin=plugin;

Check warning on line 505 in src/test/java/net/tfminecraft/cooking/farming/FarmingCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

getPluginLoader() in org.bukkit.plugin.java.JavaPlugin has been deprecated and marked for removal

Check warning on line 505 in src/test/java/net/tfminecraft/cooking/farming/FarmingCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

getPluginLoader() in org.bukkit.plugin.java.JavaPlugin has been deprecated and marked for removal
settings=new StaticConfigSnapshot(FarmingConfig.class,CropsConfig.class,CropPlantingRule.class,CropCustomCropsBridge.class,QualityConfig.class); QualityConfig.apply(1,5,null,null); CropPlantingRule.configure(null); CropsConfig.apply(Map.of(),Map.of(),Map.of());
previousFoods=FoodLoader.oList; FoodLoader.oList=new ArrayList<>(); previousModels=ModelLoader.models; ModelLoader.models=new ArrayList<>(); previousConversions=ConversionLoader.conversions; ConversionLoader.conversions=new LinkedHashMap<>();
config.loadFromString("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,15 @@ class CommandManagerCoverageTest {
}).when(manager).savePlayer(player);
run("food", "Admin", "100");
assertEquals(100, character.getFoodValue());
assertTrue(messages().isEmpty());
assertEquals(List.of("搂cCould not confirm Admin's food was saved: they switched character."), messages());
display.verify(() -> NutritionDisplayService.sync(any(net.tfminecraft.cooking.nutrition.NutritionContext.class), anyString()), never());
doNothing().when(manager).savePlayer(player);
display.when(() -> NutritionDisplayService.sync(any(net.tfminecraft.cooking.nutrition.NutritionContext.class), eq("admin")))
.thenAnswer(call -> { identities.set(player, null); return null; });
run("food", "Admin", "120");
assertEquals(120, character.getFoodValue());
assertTrue(messages().isEmpty());
assertEquals(List.of("搂cAdmin's food was saved, but they switched character before their hunger bar updated."),
messages());
run("food", "Admin", "130");
assertEquals(120, character.getFoodValue());
assertEquals(List.of("搂cThat player has no active character."), messages());
Expand Down
Loading
Loading