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
1 change: 1 addition & 0 deletions .github/dependencies.sha256
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
225aa7f75d4e6e6ae6b3ad87a7ca678c025c027f00374ab1062275d24f3a73c9 libs/MythicLib-1.7.1-SNAPSHOT.jar
81d511d0830951f988615b6533cc93e82d00a83fb31e8ccbb36441ae3e8157ef libs/MMOCore-1.13.1-SNAPSHOT.jar
6df72b5b331d5d8881f2b0ab99cbaa236dd941cbbbaf69fbd05dcb412bffb7f6 libs/MythicMobs-5.13.1-SNAPSHOT-88530541.jar
5a01b37bd7442fb0f2c809cadac82ba70f75173956e04efac3336595cc070368 libs/ItemsAdder-4.0.18.jar
355f7117af95b3f8e3cb63440fcf8a3bb9808bc89ac6ac5c57142327480e9437 libs/ProtocolLib-5.5.0-SNAPSHOT.jar
44ee292392dd86b4aa4824c2e11668a9dee0333b83ae013987e1ba78d19f51a0 libs/ModelEngine-R4.1.1.jar
3 changes: 3 additions & 0 deletions .github/scripts/install-local-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.
mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \
-Dfile="libs/MMOCore-1.13.1-SNAPSHOT.jar" -DgroupId="local" -DartifactId="MMOCore" \
-Dversion="1.13.1-SNAPSHOT-tfmc-81d511d08309" -Dpackaging=jar -DgeneratePom=true "$@"
mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \
-Dfile="libs/MythicMobs-5.13.1-SNAPSHOT-88530541.jar" -DgroupId="local" -DartifactId="MythicMobs" \
-Dversion="5.13.1-SNAPSHOT-88530541-tfmc-6df72b5b331d" -Dpackaging=jar -DgeneratePom=true "$@"

mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \
-Dfile="libs/ItemsAdder-4.0.18.jar" -DgroupId="local" -DartifactId="ItemsAdder" \
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/prepare-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mkdir -p libs
curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/355f7117af95/ProtocolLib-5.5.0-SNAPSHOT.jar?ref=$ref" > "libs/ProtocolLib-5.5.0-SNAPSHOT.jar"
curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/225aa7f75d4e/MythicLib-1.7.1-SNAPSHOT.jar?ref=$ref" > "libs/MythicLib-1.7.1-SNAPSHOT.jar"
curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/81d511d08309/MMOCore-1.13.1-SNAPSHOT.jar?ref=$ref" > "libs/MMOCore-1.13.1-SNAPSHOT.jar"
curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/6df72b5b331d/MythicMobs-5.13.1-SNAPSHOT-88530541.jar?ref=$ref" > "libs/MythicMobs-5.13.1-SNAPSHOT-88530541.jar"
curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/5a01b37bd744/ItemsAdder-4.0.18.jar?ref=$ref" > "libs/ItemsAdder-4.0.18.jar"
curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/44ee292392dd/ModelEngine-R4.1.1.jar?ref=$ref" > "libs/ModelEngine-R4.1.1.jar"
bash .github/scripts/install-local-dependencies.sh "$@"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ These features give other TF-Minecraft plugins common building blocks while also
- **Required pack on join** — opt-in (`resource-pack.require-on-join`); sends the pack as required and publishes `tfmccore:required-pack` player metadata (`loading`, `ready`, `failed`) for RPCharacters. Staff retry a failed load with `/tcore packretry <player>`.
- **Book appearance** — optionally hides the default glint on signed books by storing a glint override on the books (`hide-book-glint`).
- **Book copies** — written books can be copied with a Book and Quill at a crafting table, even though MMOItems blocks vanilla recipes that use a disable-crafting MMOItem (`allow-book-copies`).
- **Wounded healing** — anyone under the Wounded status (a MythicMobs aura from TFMC class skills) heals by `wounded-healing-multiplier` (0.5): lifesteal, skill heals, food and MMOCore regeneration alike. Absorption shields are not affected.
- **Accurate damage indicators** — MythicLib's floating damage numbers show the health the target really lost after armour, absorption hearts included, one tick after the hit. Regeneration, other heals and other damage in that tick don't move it, a killing blow shows the health that was left, and a hit that took nothing shows nothing (`accurate-damage-indicators`).
- **Cross-plugin statistics** — records supported vehicle, character, crafting, skill, and faction events for staff queries.

Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
<version>1.13.1-SNAPSHOT-tfmc-81d511d08309</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>local</groupId>
<artifactId>MythicMobs</artifactId>
<version>5.13.1-SNAPSHOT-88530541-tfmc-6df72b5b331d</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>local</groupId>
<artifactId>ItemsAdder</artifactId>
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/TFMCCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import net.tfminecraft.tfmccore.commands.SilentPermissionCommand;
import net.tfminecraft.tfmccore.golem.GolemListener;
import net.tfminecraft.tfmccore.health.MaxHealthRounder;
import net.tfminecraft.tfmccore.health.WoundedHealing;
import net.tfminecraft.tfmccore.indicators.RealDamageIndicators;
import net.tfminecraft.tfmccore.movement.SoulSandStride;
import net.tfminecraft.tfmccore.loader.ConfigLoader;
Expand Down Expand Up @@ -84,6 +85,7 @@ public class TFMCCore extends JavaPlugin{
private BookCopyUnblocker bookCopies;
private MaxHealthRounder maxHealth;
private SoulSandStride soulSand;
private WoundedHealing wounded;

@Override
public void onEnable() {
Expand Down Expand Up @@ -192,6 +194,7 @@ public void onDisable() {
if (bookCopies != null) bookCopies.unwrap();
if (maxHealth != null) maxHealth.close();
if (soulSand != null) soulSand.close();
if (wounded != null) wounded.close();
if (stoneListener != null) {
stoneListener.refundAll();
}
Expand Down Expand Up @@ -354,6 +357,11 @@ public void registerListeners() {
RealDamageIndicators.register(this, new File(
getServer().getPluginManager().getPlugin("MythicLib").getDataFolder(), "indicators.yml"));
}
// The Wounded status is a MythicMobs aura; the regeneration cut is a MythicLib stat modifier
if (getServer().getPluginManager().isPluginEnabled("MythicMobs")
&& getServer().getPluginManager().isPluginEnabled("MythicLib")) {
wounded = WoundedHealing.register(this);
}
}

public void createConfigs() {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/cache/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class Cache {
public static boolean allowBookCopies = true;
public static volatile boolean soulSandStride = false;
public static boolean accurateDamageIndicators = true;
// Read on every heal
public static volatile double woundedHealingMultiplier = 0.5;

public static int armourTime = 7;

Expand Down
148 changes: 148 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/health/WoundedHealing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package net.tfminecraft.tfmccore.health;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.function.DoubleSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;

import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;

import io.lumine.mythic.bukkit.BukkitAdapter;
import io.lumine.mythic.bukkit.MythicBukkit;
import io.lumine.mythic.lib.api.player.MMOPlayerData;
import io.lumine.mythic.lib.api.stat.StatInstance;
import io.lumine.mythic.lib.api.stat.modifier.StatModifier;
import io.lumine.mythic.lib.player.modifier.ModifierType;
import net.tfminecraft.tfmccore.cache.Cache;

/**
* Cuts the healing of anyone under the Wounded status.
*
* <p>Wounded is a MythicMobs aura that TFMC class skills give ({@code tfmc_status_skills.yml}).
* Heals that reach {@link EntityRegainHealthEvent} use the amount it ends with: vanilla
* regeneration and food, MythicLib's {@code heal} (lifesteal and spell vampirism) and MythicMobs'
* {@code heal}, which fires its own heal event and then this one. So one listener covers them, and
* none is cut twice.
*
* <p>MMOCore's health regeneration sets the health directly and fires no heal event, so for Wounded
* players this also scales the two MythicLib stats it reads ({@link #REGEN_STATS}) by the same
* multiplier, checked every {@link #PERIOD} ticks. The modifier is taken off when the status ends,
* at quit and on disable. Absorption shields are not healing and stay as they are.
*
* <p>Regeneration is cut once only because MMOCore 1.13.1 never registers MythicLib's
* {@code HealthUpdateEventSupplier}, so its bridge from regeneration to {@link EntityRegainHealthEvent}
* never runs. Re-check this after an MMOCore update: with that bridge, the stat modifier and the
* listener would both cut regeneration.
*/
public final class WoundedHealing implements Listener {

/** The MythicMobs aura name of the status. */
static final String AURA = "Wounded";
/** The MythicLib stats MMOCore health regeneration reads: flat per second, and % of max health. */
static final String[] REGEN_STATS = {"HEALTH_REGENERATION", "MAX_HEALTH_REGENERATION"};
/** Key of the regeneration modifier. */
static final String KEY = "tfmccore_wounded";
/** 5 ticks: the regeneration cut starts and ends within a quarter of a second of the status. */
static final long PERIOD = 5L;

/** Scales a player's regeneration stats; factor 1 takes the scaling off. False if it could not yet. */
interface RegenScale {
boolean set(Player player, double factor);
}

private final Predicate<Entity> wounded;
private final DoubleSupplier multiplier;
private final Supplier<Collection<? extends Player>> players;
private final RegenScale regen;
private final Set<UUID> scaled = new HashSet<>();
private double applied = 1.0;

WoundedHealing(Predicate<Entity> wounded, DoubleSupplier multiplier,
Supplier<Collection<? extends Player>> players, RegenScale regen) {
this.wounded = wounded;
this.multiplier = multiplier;
this.players = players;
this.regen = regen;
}

/** Listens for heals and checks regeneration. Needs MythicMobs (the status) and MythicLib (the stats). */
public static WoundedHealing register(JavaPlugin plugin) {
WoundedHealing healing = new WoundedHealing(WoundedHealing::hasAura, () -> Cache.woundedHealingMultiplier,
() -> plugin.getServer().getOnlinePlayers(), WoundedHealing::scaleRegen);
plugin.getServer().getPluginManager().registerEvents(healing, plugin);
plugin.getServer().getScheduler().runTaskTimer(plugin, healing::tick, PERIOD, PERIOD);
return healing;
}

/** Read-only: getAuraRegistry would create and keep a registry for every entity it is asked about. */
static boolean hasAura(Entity entity) {
return MythicBukkit.inst().getSkillManager().getAuraManager().getHasAura(BukkitAdapter.adapt(entity), AURA);
}

static boolean scaleRegen(Player player, double factor) {
MMOPlayerData data = MMOPlayerData.getOrNull(player);
if (data == null) return false; // MythicLib has not loaded the player yet; the next tick tries again
for (String stat : REGEN_STATS) {
StatInstance instance = data.getStatMap().getInstance(stat);
instance.remove(KEY);
if (factor < 1.0) instance.addModifier(new StatModifier(KEY, stat, (factor - 1.0) * 100.0, ModifierType.COMPOUND));
}
return true;
}

private double factor() {
double value = multiplier.getAsDouble();
if (Double.isNaN(value)) return 1.0;
return Math.max(0.0, Math.min(1.0, value));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/** HIGH: after plugins that change heals at NORMAL, before MONITOR listeners read the result. */
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onRegainHealth(EntityRegainHealthEvent event) {
double factor = factor();
if (factor >= 1.0 || !wounded.test(event.getEntity())) return;
event.setAmount(event.getAmount() * factor);
}

/** Gives every Wounded player the regeneration cut and takes it off the others that had it. */
void tick() {
double factor = factor();
boolean changed = factor != applied; // a config reload changed the multiplier
applied = factor;
Set<UUID> online = new HashSet<>();
for (Player player : players.get()) {
UUID id = player.getUniqueId();
online.add(id);
boolean cut = factor < 1.0 && wounded.test(player);
if (cut && (!scaled.contains(id) || changed)) {
if (regen.set(player, factor)) scaled.add(id);
else scaled.remove(id); // not applied: try again on the next tick
} else if (!cut && scaled.remove(id)) regen.set(player, 1.0);
}
scaled.retainAll(online);
}

/** Takes the cut off at quit, so it never waits in MythicLib's player data for the next login. */
@EventHandler
public void onQuit(PlayerQuitEvent event) {
if (scaled.remove(event.getPlayer().getUniqueId())) regen.set(event.getPlayer(), 1.0);
}

/** Takes the regeneration cut off everyone online who has it. Runs on disable. */
public void close() {
for (Player player : players.get()) {
if (scaled.remove(player.getUniqueId())) regen.set(player, 1.0);
}
scaled.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public boolean loadConfig(File configFile) {
Cache.allowBookCopies = config.getBoolean("allow-book-copies", true);
Cache.soulSandStride = config.getBoolean("soul-sand-stride", false);
Cache.accurateDamageIndicators = config.getBoolean("accurate-damage-indicators", true);
Cache.woundedHealingMultiplier = config.getDouble("wounded-healing-multiplier", 0.5);

Cache.armourTime = config.getInt("armour-time", 7);

Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ soul-sand-stride: false
# tick later; a killing blow shows the health that was left, a hit that took nothing shows
# nothing. false shows MythicLib's own number, which leaves armour out.
accurate-damage-indicators: true
# Healing on anyone under the Wounded status (a MythicMobs aura that TFMC class skills give)
# is multiplied by this: lifesteal, skill heals, regeneration and food alike. Absorption
# shields are not healing. 1 turns it off; it needs MythicMobs and MythicLib.
wounded-healing-multiplier: 0.5

drops-debug: true

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ version: ${project.version}
author: Drefvelin

depend: [TLibs]
softdepend: [ItemsAdder, ProtocolLib, VehicleFramework, RPCharacters, AdvancedCrafting, MythicLib, MMOCore, SimpleFactions, LuckPerms]
softdepend: [ItemsAdder, ProtocolLib, VehicleFramework, RPCharacters, AdvancedCrafting, MythicLib, MythicMobs, MMOCore, SimpleFactions, LuckPerms]

commands:
tcore:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@
.when(Bukkit::getUnsafe)
.thenReturn(mock(org.bukkit.UnsafeValues.class, RETURNS_DEEP_STUBS));
services
.when(() -> net.kyori.adventure.util.Services.service(PluginLoader.class))

Check warning on line 265 in src/test/java/net/tfminecraft/tfmccore/PluginLifecycleCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

org.bukkit.plugin.PluginLoader in org.bukkit.plugin has been deprecated and marked for removal
.thenReturn(Optional.of(mock(PluginLoader.class)));

Check warning on line 266 in src/test/java/net/tfminecraft/tfmccore/PluginLifecycleCoverageTest.java

View workflow job for this annotation

GitHub Actions / build

org.bukkit.plugin.PluginLoader in org.bukkit.plugin has been deprecated and marked for removal
TestPluginLoader loader = new TestPluginLoader(description, server, logger, directory);

JavaPlugin plugin =
Expand Down Expand Up @@ -457,6 +457,33 @@
}
}

@ParameterizedTest
@ValueSource(strings = {"both", "mythicmobs-only", "mythiclib-only", "neither"})
void woundedHealingNeedsMythicMobsAndMythicLibAndClosesOnDisable(String plugins) throws Exception {
boolean mythicMobs = plugins.equals("both") || plugins.equals("mythicmobs-only");
boolean mythicLib = plugins.equals("both") || plugins.equals("mythiclib-only");
try (Rig rig = new Rig();
var wounded = mockStatic(net.tfminecraft.tfmccore.health.WoundedHealing.class);
var rounders = mockStatic(net.tfminecraft.tfmccore.health.MaxHealthRounder.class);
var indicators = mockStatic(net.tfminecraft.tfmccore.indicators.RealDamageIndicators.class)) {
var healing = mock(net.tfminecraft.tfmccore.health.WoundedHealing.class);
wounded.when(() -> net.tfminecraft.tfmccore.health.WoundedHealing.register(rig.plugin)).thenReturn(healing);
when(rig.manager.isPluginEnabled("MythicMobs")).thenReturn(mythicMobs);
when(rig.manager.isPluginEnabled("MythicLib")).thenReturn(mythicLib);
Plugin mythicLibPlugin = mock(Plugin.class);
when(mythicLibPlugin.getDataFolder()).thenReturn(new File("MythicLib"));
when(rig.manager.getPlugin("MythicLib")).thenReturn(mythicLibPlugin);

rig.plugin.onEnable();
rig.plugin.onDisable();

boolean loads = mythicMobs && mythicLib;
wounded.verify(
() -> net.tfminecraft.tfmccore.health.WoundedHealing.register(rig.plugin), times(loads ? 1 : 0));
verify(healing, times(loads ? 1 : 0)).close();
}
}

@ParameterizedTest
@ValueSource(strings = {"off", "absent", "standard", "multipart"})
void requiredPackGateIsOptInAndOwnsManualSendsWhileLoading(String scenario) throws Exception {
Expand Down
Loading
Loading