feat(events): record Sanctuary area and healing outcomes
Release / release (push) Successful in 7m19s

This commit is contained in:
dmg committed 2026-09-25 03:02:45 -04:00
1 parent 21877f6820
commit ff28f2e98d
7 files changed
+181 -21

No files matched your search

@@ -729,6 +729,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
abilityEngine.enableCastEventsFor("rallying_cry");
abilityEngine.enableCastEventsFor("frost_nova");
abilityEngine.enableCastEventsFor("chain_lightning");
abilityEngine.enableCastEventsFor("sanctuary");
admin.attachAbilityConfigEvents(new games.dmg.fantasystick.events.AbilityConfigEventBridge(eventLog::emit, Clock.systemUTC()));
passiveEvents = new games.dmg.fantasystick.events.PassiveEventBridge(eventLog::emit, Clock.systemUTC());books.attachPassiveEvents(passiveEvents);admin.attachPassiveEvents(passiveEvents);
recoveryEvents = new games.dmg.fantasystick.events.RecoveryEventBridge(eventLog::emit, Clock.systemUTC());outOfCombatRecovery.attachEvents(recoveryEvents,record->record.passives().contains(games.dmg.fantasystick.passive.QuickerRegenerationI.ID)?quickerRegeneration.bonus():java.math.BigInteger.ZERO);
@@ -18,9 +18,12 @@ import org.bukkit.event.entity.EntityDamageEvent;
/** Internal strongest-reduction stage shared by Fantasy Stick protection effects. */
public final class CombatMitigation implements Listener, AutoCloseable {
private final class Grant implements AutoCloseable {
final AbilityExecution execution; final Player player; final BigDecimal percent; final BooleanSupplier current; boolean closed;
Grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current) {
final AbilityExecution execution; final Player player; final BigDecimal percent; final BooleanSupplier current;
final java.util.function.BiConsumer<EntityDamageEvent, Double> acceptedObservation; boolean closed;
Grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current,
java.util.function.BiConsumer<EntityDamageEvent, Double> acceptedObservation) {
this.execution = execution; this.player = player; this.percent = percent; this.current = current;
this.acceptedObservation = acceptedObservation;
}
boolean valid() {
try { return !closed && execution.stillCurrent() && current.getAsBoolean(); }
@@ -42,10 +45,16 @@ public final class CombatMitigation implements Listener, AutoCloseable {
}
public AutoCloseable grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current) {
return grant(execution, player, percent, current, null);
}
/** An owner may observe only the selected, positive native prevention after final cancellation. */
public AutoCloseable grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current,
java.util.function.BiConsumer<EntityDamageEvent, Double> acceptedObservation) {
Objects.requireNonNull(execution); Objects.requireNonNull(player); Objects.requireNonNull(percent); Objects.requireNonNull(current);
if (closed || percent.signum() < 0 || percent.compareTo(BigDecimal.valueOf(100)) > 0)
throw new IllegalArgumentException("Invalid combat mitigation grant");
var grant = new Grant(execution, player, percent, current);
var grant = new Grant(execution, player, percent, current, acceptedObservation);
grants.computeIfAbsent(player.getUniqueId(), ignored -> new ArrayList<>()).add(grant);
return grant;
}
@@ -76,6 +85,9 @@ public final class CombatMitigation implements Listener, AutoCloseable {
if (prevention == null || event.isCancelled() || event.getFinalDamage() <= 0
|| !prevention.grant.valid()) return;
prevention.grant.execution.report(AbilityReport.Type.PREVENTION, event.getEntity(), prevention.amount);
if (prevention.grant.acceptedObservation != null) try {
prevention.grant.acceptedObservation.accept(event, prevention.amount.doubleValue());
} catch (RuntimeException ignored) { /* Audit cannot alter the accepted native hit. */ }
}
@Override public void close() {
@@ -53,23 +53,30 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
private final class State implements AutoCloseable {
final AbilityExecution execution; final Player source;
final Location origin; final Settings captured; final Map<UUID, AutoCloseable> mitigation = new LinkedHashMap<>();
final Location origin; final Settings captured; final Map<UUID, Benefit> mitigation = new LinkedHashMap<>();
Duration elapsed = Duration.ZERO; Duration nextPulse; final Duration scanPeriod;
int pending; boolean expired; boolean closed;
UUID areaApplication; int pulseOrdinal;
String endReason = "interrupted"; int pending; boolean expired; boolean closed;
State(AbilityExecution execution, Settings captured) {
this.execution = execution; this.source = execution.input().player(); this.origin = source.getLocation().clone();
this.captured = captured; this.nextPulse = captured.pulseInterval();
long period = Math.max(50_000_000L, (captured.duration().toNanos() + 59) / 60);
this.scanPeriod = Duration.ofNanos(period);
}
void start() { states.add(this); execution.own(this); reconcile(); visual(); schedule(); }
void start() {
states.add(this); execution.own(this);
areaApplication = execution.effectApplied("sanctuary_area", null,
Map.of("radius", captured.radius(), "vertical_range", captured.verticalRange(),
"duration_seconds", seconds(captured.duration()), "area_location", locationData(origin)));
reconcile(); visual(); schedule();
}
void schedule() {
if (closed || expired) return;
Duration remaining = captured.duration().minus(elapsed);
execution.later(remaining.compareTo(scanPeriod) < 0 ? remaining : scanPeriod, this::step);
}
void step() {
if (closed || !execution.stillCurrent()) { execution.finish(); return; }
if (closed || !execution.stillCurrent()) { execution.resolve("interrupted"); execution.finish(); return; }
Duration remaining = captured.duration().minus(elapsed);
elapsed = elapsed.plus(remaining.compareTo(scanPeriod) < 0 ? remaining : scanPeriod);
reconcile(); visual();
@@ -77,20 +84,22 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
pulse(); nextPulse = nextPulse.plus(captured.pulseInterval());
}
if (elapsed.compareTo(captured.duration()) >= 0) {
expired = true; closeMitigation(); if (pending == 0) execution.finish();
expired = true; endReason = "expired"; closeMitigation();
execution.resolve("expired", Map.of("pulse_count", pulseOrdinal));
if (pending == 0) execution.finish();
} else schedule();
}
void reconcile() {
var eligible = recipients();
for (var entry : List.copyOf(mitigation.entrySet())) {
if (eligible.stream().noneMatch(player -> player.getUniqueId().equals(entry.getKey()))) {
try { entry.getValue().close(); } catch (Exception ignored) { } mitigation.remove(entry.getKey());
entry.getValue().close(); mitigation.remove(entry.getKey());
}
}
for (var player : eligible) mitigation.computeIfAbsent(player.getUniqueId(), ignored ->
combatMitigation.grant(execution, player, captured.damageReductionPercent(), () -> recipientCurrent(player)));
for (var player : eligible) mitigation.computeIfAbsent(player.getUniqueId(), ignored -> new Benefit(this, player));
}
void pulse() {
pulseOrdinal++;
for (var player : recipients()) if (owner(player) == this) heal(player);
}
void heal(Player player) {
@@ -141,11 +150,46 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
captured.radius() / 2, 0.1, captured.radius() / 2, 0); } catch (RuntimeException ignored) { }
}
void closeMitigation() {
for (var grant : List.copyOf(mitigation.values())) try { grant.close(); } catch (Exception ignored) { }
for (var grant : List.copyOf(mitigation.values())) grant.close();
mitigation.clear();
}
@Override public void close() {
if (closed) return; closed = true; expired = true; states.remove(this); closeMitigation();
if (areaApplication != null) execution.effectEnded(areaApplication, endReason);
}
}
private final class Benefit implements AutoCloseable {
final State state; final Player player; final AutoCloseable grant;
UUID application; boolean closed;
Benefit(State state, Player player) {
this.state = state; this.player = player;
grant = combatMitigation.grant(state.execution, player, state.captured.damageReductionPercent(),
() -> !closed && state.recipientCurrent(player), (event, amount) -> {
var active = sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);
var attacker = event.getDamageSource().getCausingEntity();
if (attacker == null && event instanceof org.bukkit.event.entity.EntityDamageByEntityEvent byEntity)
attacker = byEntity.getDamager();
if (active != null && attacker != null && amount > 0)
state.execution.acceptedPrevention(active, player, attacker,
event.getDamage() + amount, amount, event.getFinalDamage(), application);
});
try {
var active = sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);
var evidence = new LinkedHashMap<String, Object>();
evidence.put("damage_reduction_percent", state.captured.damageReductionPercent().toPlainString());
if (active != null) {
evidence.put("recipient_character_id", active.character().toString());
evidence.put("recipient_session_id", active.session().toString());
}
application = state.execution.effectApplied("sanctuary_benefit", player, evidence);
} catch (RuntimeException ignored) { /* Auditing cannot refuse a native mitigation grant. */ }
}
@Override public void close() {
if (closed) return; closed = true;
try { grant.close(); } catch (Exception ignored) { }
if (application != null) state.execution.effectEnded(application,
state.closed || state.expired ? state.endReason : "recipient_eligibility_changed");
}
}
@@ -209,10 +253,15 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
double desired = Math.min(maximum, persisted.health().doubleValue()); double restored = Math.min(accepted, desired - current);
if (!Double.isFinite(current) || !Double.isFinite(maximum) || !Double.isFinite(restored) || restored <= 0) return;
target.player().setHealth(Math.min(maximum, current + restored));
double after = target.player().getHealth();
double actual = after - current;
if (!Double.isFinite(actual) || actual <= 0) return;
state.execution.acceptedHeal(target.active(), target.player(), persisted,
accepted, actual, current, after, maximum);
state.execution.report(AbilityReport.Type.HEALING, target.player(),
BigDecimal.valueOf(restored).stripTrailingZeros());
BigDecimal.valueOf(actual).stripTrailingZeros());
if (target.player() != state.source) supports.accept(new MendingLight.Support(
state.execution.input().session().character(), target.active(), BigDecimal.valueOf(restored).stripTrailingZeros()));
state.execution.input().session().character(), target.active(), BigDecimal.valueOf(actual).stripTrailingZeros()));
} finally { completePending(state); }
}
private static void completePending(State state) {
@@ -240,6 +289,13 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
return world.rayTraceBlocks(origin, offset.multiply(1 / distance), distance, org.bukkit.FluidCollisionMode.NEVER, true) == null;
} catch (RuntimeException invalid) { return false; }
}
private static Map<String, Object> locationData(Location location) {
var world = location == null ? null : location.getWorld();
if (world == null || world.getUID() == null || world.getName() == null)
return Map.of("location_status", "unavailable");
return Map.of("world_id", world.getUID().toString(), "world_name", world.getName(),
"x", location.getX(), "y", location.getY(), "z", location.getZ());
}
private static double distance(Location origin, Player player) {
try { var at = player.getLocation(); if (origin.getWorld() != at.getWorld()) return Double.POSITIVE_INFINITY;
double dx = at.getX() - origin.getX(), dz = at.getZ() - origin.getZ(); return dx * dx + dz * dz;
@@ -73,10 +73,11 @@ public final class SanctuarySettings implements AutoCloseable {
public String value(String key) { return effectiveValues.get(key); }
public Sanctuary.Settings settings() { return parse(effectiveValues); }
public synchronized void publish(Update update) {
public synchronized boolean publish(Update update) {
Objects.requireNonNull(update);
if (update.sequence() <= publishedSequence) return;
if (update.sequence() <= publishedSequence) return false;
parse(update.values()); effectiveValues = update.values(); publishedSequence = update.sequence();
return true;
}
public CompletionStage<Update> set(String key, String value) {
@@ -1453,8 +1453,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
private void sanctuaryConfiguration(CommandSender sender, String[] args) {
String key = args[1];
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
String before = sanctuarySettings.value(key);
if (args.length == 2) {
sender.sendMessage(key + "=" + sanctuarySettings.value(key) + " (applies to future Sanctuary casts only)"); return;
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "sanctuary", key, before);
sender.sendMessage(key + "=" + before + " (applies to future Sanctuary casts only)"); return;
}
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.SanctuarySettings.Update> operation;
try { operation = sanctuarySettings.set(key, args[2]); }
@@ -1466,12 +1469,14 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
if (closed) return;
try { nextTick.accept(() -> {
if (closed) return;
if (failure == null) {
sanctuary.configure(saved.settings()); abilities.replace(sanctuary.definition()); sanctuarySettings.publish(saved);
boolean published = failure == null && sanctuarySettings.publish(saved);
if (published) {
sanctuary.configure(saved.settings()); abilities.replace(sanctuary.definition());
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "sanctuary", key, before, saved.value());
}
if (!sender.hasPermission(PERMISSION)
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
sender.sendMessage(failure == null
sender.sendMessage(published
? "Saved " + key + "=" + saved.value() + " for future Sanctuary casts; active areas and deadlines are unchanged."
: "Sanctuary configuration save failed; the effective setting is unchanged.");
}); } catch (RuntimeException rejected) { report(null, "config-response", rejected); }
@@ -43,6 +43,19 @@ class SanctuaryNativeTest {
var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);fixture.players.put(viewer.core.id,viewer.player);
PartyBookNativeTest.disableAllParticles(fixture,caster);caster.particleCalls.clear();viewer.particleCalls.clear();
use(fixture,caster);drain(fixture,2);assertEquals(2L,fixture.delayedTicks.getLast());
var incomingAttacker=mock(org.bukkit.entity.LivingEntity.class);when(incomingAttacker.getUniqueId()).thenReturn(UUID.randomUUID());
var incomingSource=DamageSource.builder(org.bukkit.damage.DamageType.MOB_ATTACK)
.withCausingEntity(incomingAttacker).withDirectEntity(incomingAttacker).build();
var incoming=new EntityDamageByEntityEvent(incomingAttacker,ally.player,EntityDamageEvent.DamageCause.ENTITY_ATTACK,incomingSource,10);
fixture.dispatcher.accept(incoming);assertEquals(8,incoming.getFinalDamage(),1.0e-9);
class LateVeto implements org.bukkit.event.Listener {
@org.bukkit.event.EventHandler(priority=org.bukkit.event.EventPriority.HIGHEST)
public void cancel(EntityDamageByEntityEvent event){if(event.getDamager()==incomingAttacker)event.setCancelled(true);}
}
fixture.dispatcher.register(new LateVeto(),fixture.plugin);
var vetoed=new EntityDamageByEntityEvent(incomingAttacker,ally.player,
EntityDamageEvent.DamageCause.ENTITY_ATTACK,incomingSource,10);
fixture.dispatcher.accept(vetoed);assertTrue(vetoed.isCancelled());
for(int i=0;i<10;i++)fixture.delayed.removeFirst().run();drain(fixture,4);assertEquals(6,ally.core.health,0);
assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no Sanctuary particles");assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==org.bukkit.Particle.END_ROD&&call[2].equals(12)),"An enabled nearby viewer retains Sanctuary visuals");
var enemy=mock(org.bukkit.entity.LivingEntity.class);when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());when(enemy.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
@@ -54,6 +67,61 @@ class SanctuaryNativeTest {
assertEquals(BigInteger.valueOf(5),storage.find(caster.core.record.id()).get().orElseThrow().experience());
assertEquals(BigInteger.valueOf(5),storage.find(ally.core.record.id()).get().orElseThrow().experience());
}
var records=events(data);
for(var type:List.of("ability.learned","ability.assigned","ability.cast","ability.resolved","combat.prevented"))
assertEquals(1,abilityEvents(records,type).size(),type);
var heals=abilityEvents(records,"combat.healed");
assertTrue(heals.size()>=1,"Actual acknowledged ally-health pulses are individually recorded");
assertEquals(heals.size(),heals.stream().map(event->event.getAsJsonObject("data").get("application_id").getAsString()).distinct().count());
assertEquals(3,abilityEvents(records,"effect.applied").size(),"One fixed area plus caster/ally benefits");
assertEquals(3,abilityEvents(records,"effect.ended").size(),"No per-tick duplicate grants");
var heal=heals.stream().map(event->event.getAsJsonObject("data"))
.filter(value->value.get("health_before").getAsString().equals("4")).findFirst().orElseThrow();
assertEquals("2",heal.get("actual_amount").getAsString());
assertEquals("6",heal.get("health_after").getAsString());
assertTrue(heal.has("target_location"));
assertEquals("2",abilityEvents(records,"combat.prevented").getFirst().getAsJsonObject("data")
.get("actual_amount").getAsString());
}
@Test void cancelledNativeAllyHealCannotClaimSavedOrEffectiveHealth()throws Exception{
UUID allyId;
try(var fixture=new FantasyStickLifecycleNativeTest.Fixture(data)){
fixture.plugin.setEnabled(true);
var caster=new FantasyStickLifecycleNativeTest.Actor(data);
var ally=new FantasyStickLifecycleNativeTest.Actor(data);allyId=ally.core.id;
prepareCaster(data,caster,true);setHealth(data,ally,BigDecimal.valueOf(4));
fixture.players.put(caster.core.id,caster.player);fixture.players.put(allyId,ally.player);
fixture.activate(caster);fixture.activate(ally);party(fixture,caster,ally);
caster.core.location=new Location(caster.world,0,64,0);ally.core.location=new Location(caster.world,2,64,0);
doReturn(List.of(ally.player)).when(caster.world).getNearbyEntities(any(Location.class),anyDouble(),anyDouble(),anyDouble());
class Veto implements org.bukkit.event.Listener{
@org.bukkit.event.EventHandler(priority=org.bukkit.event.EventPriority.HIGH)
public void cancel(EntityRegainHealthEvent event){if(event.getEntity()==ally.player)event.setCancelled(true);}
}
fixture.dispatcher.register(new Veto(),fixture.plugin);
use(fixture,caster);drain(fixture,2);
for(int i=0;i<10;i++)fixture.delayed.removeFirst().run();drain(fixture,4);
assertEquals(4,ally.core.health,0);
}
assertTrue(abilityEvents(events(data),"combat.healed").stream()
.noneMatch(event->event.getAsJsonObject("data").get("target_player_id").getAsString().equals(allyId.toString())),
"Cancelled ally healing must not become a saved-health success or a JSONL healing claim");
}
private static List<com.google.gson.JsonObject> events(Path root)throws Exception{
var values=new ArrayList<com.google.gson.JsonObject>();
try(var files=java.nio.file.Files.walk(root.resolve("events"))){
for(var file:files.filter(path->path.toString().endsWith(".jsonl")).toList())
for(var line:java.nio.file.Files.readAllLines(file))
if(!line.isBlank())values.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
}
return values;
}
private static List<com.google.gson.JsonObject> abilityEvents(List<com.google.gson.JsonObject> events,String suffix){
return events.stream().filter(event->event.get("type").getAsString().endsWith(suffix)
&& event.getAsJsonObject("data").has("ability_id")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("sanctuary")).toList();
}
@Test void fixedCylinderHasNoImmediatePulseThenSixOwnedHealingAndMitigationPulses() {
@@ -145,6 +213,16 @@ class SanctuaryNativeTest {
plugin.command.execute(sender.port,"fantasystick",new String[]{"config",update[0],update[1]});var response=plugin.tasks.poll(5,TimeUnit.SECONDS);assertNotNull(response);response.run();}
var configured=registry.find("sanctuary").orElseThrow();assertEquals(new BigDecimal("4"),configured.parameters().get("healing-per-pulse"));
assertEquals(new BigDecimal("30"),configured.parameters().get("damage-reduction-percent"));assertEquals(java.time.Duration.ofSeconds(20),configured.cooldown());
try(var channel=java.nio.channels.FileChannel.open(data.resolve(".config.lock"),
java.nio.file.StandardOpenOption.CREATE,java.nio.file.StandardOpenOption.WRITE);
var lock=channel.lock()){
assertTrue(lock.isValid());
plugin.command.execute(sender.port,"fstickadmin",new String[]{"config","sanctuary-radius-blocks","9"});
var failed=plugin.tasks.poll(5,TimeUnit.SECONDS);assertNotNull(failed);failed.run();
}
assertEquals("8",((games.dmg.fantasystick.persistence.SanctuarySettings)
FantasyStickLifecycleNativeTest.Fixture.field(plugin.plugin,"sanctuarySettings")).value("sanctuary-radius-blocks"));
assertTrue(sender.messages.getLast().contains("save failed"));
plugin.command.execute(sender.port,"fantasystick",new String[]{"config","sanctuary-radius-blocks","0"});assertTrue(sender.messages.getLast().contains("invalid"));
plugin.command.execute(sender.port,"fantasystick",new String[]{"config","sanctuary-radius-blocks","8","extra"});assertTrue(sender.messages.getLast().contains("Usage"));
plugin.plugin.setEnabled(false);plugin.plugin.setEnabled(true);
@@ -152,6 +230,11 @@ class SanctuaryNativeTest {
var saved=reloaded.find("sanctuary").orElseThrow();assertEquals(new BigDecimal("4"),saved.parameters().get("healing-per-pulse"));
assertEquals(new BigDecimal("30"),saved.parameters().get("damage-reduction-percent"));assertEquals(java.time.Duration.ofSeconds(20),saved.cooldown());
}
var config=events(data).stream().filter(event->event.get("type").getAsString().endsWith("config.read")
|| event.get("type").getAsString().endsWith("config.changed"))
.filter(event->event.getAsJsonObject("data").has("key")
&& event.getAsJsonObject("data").get("key").getAsString().startsWith("sanctuary-")).toList();
assertEquals(8,config.size(),"Authorized read and seven saved settings only");
}
private static void use(FantasyStickLifecycleNativeTest.Fixture fixture,FantasyStickLifecycleNativeTest.Actor actor){actor.core.inventory.getInventory().setSelectedSlot(2);fixture.dispatcher.accept(new org.bukkit.event.player.PlayerInteractEvent(actor.player,org.bukkit.event.block.Action.RIGHT_CLICK_AIR,actor.core.inventory.getItem(2),null,org.bukkit.block.BlockFace.SELF,org.bukkit.inventory.EquipmentSlot.HAND));}
@@ -22,7 +22,9 @@ class SanctuarySettingsTest {
var radius = settings.set(SanctuarySettings.RADIUS, "7").toCompletableFuture();
var cooldown = settings.set(SanctuarySettings.COOLDOWN, "30").toCompletableFuture();
var savedRadius = radius.get(5, TimeUnit.SECONDS); var savedCooldown = cooldown.get(5, TimeUnit.SECONDS);
assertEquals(6, settings.settings().radius(), 0); settings.publish(savedCooldown); settings.publish(savedRadius);
assertEquals(6, settings.settings().radius(), 0);
assertTrue(settings.publish(savedCooldown));
assertFalse(settings.publish(savedRadius), "A stale save callback cannot republish policy or claim a config event");
assertEquals(7, settings.settings().radius(), 0); assertEquals(java.time.Duration.ofSeconds(30), settings.settings().cooldown());
var yaml = (Map<?, ?>) new org.yaml.snakeyaml.Yaml().load(Files.readString(data.resolve("config.yml")));
assertEquals("retained", yaml.get("unrelated")); assertEquals("7", String.valueOf(yaml.get(SanctuarySettings.RADIUS)));