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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
| `/sharedgroup leaderboard` | Show the group's death leaderboard during a game | Group member |
| `/sharedgroup enable <health\|food\|potions>` | Enable synchronization for a stat, outside a game | OP |
| `/sharedgroup disable <health\|food\|potions>` | Disable synchronization for a stat, outside a game | OP |
| `/sharedgroup configuration` | Open the synchronization settings menu, outside a game | OP, group owner |
| `/sharedgroup version` | Display the plugin version | None |

## Food synchronization

The food level is shared across the group, but saturation is not: each player keeps their own saturation. This is a deliberate choice. Every player's hunger drains the shared food bar, so food goes down much faster than in solo play.

## What remains to be done

- Synchronize players' food levels and saturation.
- Add automated tests for the group lifecycle and stat synchronization.

Made for Minecraft 26.2 with PaperMC.
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/fr/korastrip/paperplugin/SharedStats.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public void onEnable() {
this.getServer().getPluginManager().registerEvents(new RegenerationListener(this), this);
this.getServer().getPluginManager().registerEvents(new PlayerRespawnListener(this), this);
this.getServer().getPluginManager().registerEvents(new EntityResurrectListener(this), this);
this.getServer().getPluginManager().registerEvents(new FoodListener(this), this);
this.getServer().getPluginManager().registerEvents(new InventoryClickListener(this), this);
this.getServer().getPluginManager().registerEvents(new InventoryDragListener(), this);

this.getLifecycleManager().registerEventHandler(
LifecycleEvents.COMMANDS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public boolean setEnabled(String stat, boolean enabled) {
}
}

public boolean toggle(String stat) {
return setEnabled(stat, !isEnabled(stat));
}

private String getKey(String stat) {
String normalizedStat = stat.toLowerCase(Locale.ROOT);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public LiteralCommandNode<CommandSourceStack> create() {
.then(new Stats(plugin).build())
.then(new Enable(plugin).build())
.then(new Disable(plugin).build())
.then(new Configuration(plugin).build())
.build();

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package fr.korastrip.paperplugin.commands.group.sub;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import fr.korastrip.paperplugin.SharedStats;
import fr.korastrip.paperplugin.commands.group.GroupSubCommand;
import fr.korastrip.paperplugin.enums.ErrorMessage;
import fr.korastrip.paperplugin.inventory.ConfigurationInventory;
import fr.korastrip.paperplugin.model.SharedGroup;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import org.bukkit.entity.Player;

public class Configuration extends GroupSubCommand {

public Configuration(SharedStats plugin) {
super(plugin, "configuration", "Configure SharedStats using an interface.", "");
}

@Override
public LiteralArgumentBuilder<CommandSourceStack> build() {
return Commands.literal("configuration").executes(context -> {
ContextResult ctx = this.getContext(context);
SharedGroup group = this.plugin.getGroup();

if (!(ctx.entity() instanceof Player player)) {
return this.generateError("<red>This command cannot be used by the console :)</red>", ctx.entity());
}

if(!player.getUniqueId().equals(group.getOwnerUuid())) {
return this.generateError(ErrorMessage.NOT_OWNER, player);
}

if(group.getIsPlaying()) {
return this.generateError(ErrorMessage.GAME_RUNNING, player);
}

ConfigurationInventory configurationInventory = new ConfigurationInventory(this.plugin);
player.openInventory(configurationInventory.getInventory());

return Command.SINGLE_SUCCESS;
}).requires(source -> source.getSender().isOp());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public LiteralArgumentBuilder<CommandSourceStack> build() {
return this.generateError(ErrorMessage.NO_GROUP, ctx.sender());
}

if (!group.getOwnerUuid().equals(player.getUniqueId())) {
if (!player.getUniqueId().equals(group.getOwnerUuid())) {
return this.generateError(ErrorMessage.NOT_OWNER, ctx.sender());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public LiteralArgumentBuilder<CommandSourceStack> build() {
group.setHealth(20.0);
group.syncHealth();

group.setFood(20);
group.syncFood();

players.forEach(p -> {

p.playSound(
Expand All @@ -58,6 +61,8 @@ public LiteralArgumentBuilder<CommandSourceStack> build() {
1f
);

p.setSaturation(20.0f);

p.sendRichMessage("<green>The game has started.</green> <white>Do not die!</white>");
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package fr.korastrip.paperplugin.inventory;

import fr.korastrip.paperplugin.SharedStats;
import io.papermc.paper.datacomponent.DataComponentTypes;
import io.papermc.paper.datacomponent.item.TooltipDisplay;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;

public class ConfigurationInventory implements InventoryHolder {

private final Inventory inventory;
private final SharedStats plugin;

public ConfigurationInventory(SharedStats plugin) {
this.inventory = plugin.getServer().createInventory(this, 9 * 5, Component.text("Configuration"));
this.plugin = plugin;

ItemStack whiteGlassPane = this.empty(new ItemStack(Material.WHITE_STAINED_GLASS_PANE));
ItemStack blackGlassPane = this.empty(new ItemStack(Material.BLACK_STAINED_GLASS_PANE));

ItemStack toggleFoodConfiguration = this.renameItemStack(new ItemStack(Material.COOKED_BEEF), this.getDisplayName("food"));
ItemStack toggleHealthConfiguration = this.renameItemStack(new ItemStack(Material.GLISTERING_MELON_SLICE), this.getDisplayName("health"));
ItemStack togglePotionConfiguration = this.renameItemStack(new ItemStack(Material.POTION), this.getDisplayName("potions"));

togglePotionConfiguration.setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay().addHiddenComponents(DataComponentTypes.POTION_CONTENTS).build());

this.inventory.setItem(20, toggleHealthConfiguration);
this.inventory.setItem(22, toggleFoodConfiguration);
this.inventory.setItem(24, togglePotionConfiguration);

for (int i = 0; i < 45; i++) {
if(i >= 9 && i < 36) continue;
this.inventory.setItem(i, whiteGlassPane);
}

for(int i = 9; i < 36; i++) {
if(i == 20 || i == 22 || i == 24) continue;
this.inventory.setItem(i, blackGlassPane);
}
}

@Override
public Inventory getInventory() {
return this.inventory;
}

private ItemStack renameItemStack(ItemStack item, String name) {
ItemMeta meta = item.getItemMeta();
meta.displayName(
MiniMessage.miniMessage().deserialize(name)
);
item.setItemMeta(meta);
return item;
}

private ItemStack empty(ItemStack itemStack) {
ItemMeta meta = itemStack.getItemMeta();
meta.displayName(Component.empty());
itemStack.setItemMeta(meta);
return itemStack;
}

private String isEnabled(String type) {
return this.plugin.getSynchronizationConfig().isEnabled(type) ? "<green>Enabled</green>" : "<red>Disabled</red>";
}

private String getDisplayName(String stat) {
String label = switch (stat) {
case "health" -> "Health";
case "food" -> "Food";
case "potions" -> "Potions";
default -> stat;
};

return "<white>" + label + ":</white> " + this.isEnabled(stat);
}

public void refresh(String stat) {

int slot = switch (stat) {
case "health" -> 20;
case "food" -> 22;
case "potions" -> 24;
default -> -1;
};

if (slot == -1) return;
ItemStack item = this.inventory.getItem(slot);
if (item == null) return;


item.editMeta(meta -> {
meta.displayName(
MiniMessage.miniMessage().deserialize(this.getDisplayName(stat))
);
});

this.inventory.setItem(slot, item);
}

}
46 changes: 46 additions & 0 deletions src/main/java/fr/korastrip/paperplugin/listener/FoodListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package fr.korastrip.paperplugin.listener;

import fr.korastrip.paperplugin.SharedStats;
import fr.korastrip.paperplugin.model.SharedGroup;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.FoodLevelChangeEvent;

public class FoodListener implements Listener {

private final SharedStats plugin;

public FoodListener(SharedStats plugin) {
this.plugin = plugin;
}

@EventHandler
public void onFoodLevelChange(FoodLevelChangeEvent e) {
if (e.getEntity() instanceof Player player) {
SharedGroup group = this.plugin.getGroup();

if(!this.plugin.getSynchronizationConfig().isEnabled("food")) return;
if(!group.getPlayersId().contains(player.getUniqueId())) return;
if(!group.getIsPlaying()) return;

if (e.getFoodLevel() == group.getFood()) {
e.setCancelled(true);
return;
}

if (player.getFoodLevel() == group.getFood()) {
int newFood = Math.clamp(e.getFoodLevel(), 0, 20);

group.setFood(newFood);
group.syncFood();

e.setCancelled(true);
} else {
group.syncFood();
e.setCancelled(true);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package fr.korastrip.paperplugin.listener;

import fr.korastrip.paperplugin.SharedStats;
import fr.korastrip.paperplugin.enums.ErrorMessage;
import fr.korastrip.paperplugin.inventory.ConfigurationInventory;
import fr.korastrip.paperplugin.model.SharedGroup;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;

public class InventoryClickListener implements Listener {

private final SharedStats plugin;

public InventoryClickListener(SharedStats plugin) {
this.plugin = plugin;
}

@EventHandler
public void onInventoryClickListener(InventoryClickEvent e) {
Inventory inventory = e.getInventory();
SharedGroup group = this.plugin.getGroup();

if (!(inventory.getHolder(false) instanceof ConfigurationInventory configuration)) {
return;
}

e.setCancelled(true);

if (group.getIsPlaying()) {
return;
}

ItemStack clickedItem = e.getCurrentItem();
if (clickedItem == null) return;

int slot = e.getRawSlot();

String stats;

switch (slot) {
case 20 -> {
stats = "health";
}
case 22 -> {
stats = "food";
}
case 24 -> {
stats = "potions";
}
default -> {
stats = null;
}
}

if (stats == null) return;

if (!this.plugin.getSynchronizationConfig().toggle(stats)) {
e.getWhoClicked().sendRichMessage("<red>" + ErrorMessage.CONFIG_SAVE_FAILED.getMessage() + "</red>");
return;
}

configuration.refresh(stats);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fr.korastrip.paperplugin.listener;

import fr.korastrip.paperplugin.inventory.ConfigurationInventory;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.inventory.Inventory;

public class InventoryDragListener implements Listener {

@EventHandler
public void onInventoryDrag(InventoryDragEvent e) {
Inventory inventory = e.getInventory();

if (!(inventory.getHolder(false) instanceof ConfigurationInventory)) {
return;
}

e.setCancelled(true);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public void onPlayerRespawn(PlayerRespawnEvent event) {
group.setHealth(20);
group.syncHealth();
}
if(plugin.getSynchronizationConfig().isEnabled("food")) {
group.setFood(20);
group.syncFood();
}
group.clearRespawnedPlayers();
group.isGroupDying = false;
}
Expand Down
Loading
Loading