Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
184d5520b2 |
No files matched your search
@@ -105,6 +105,8 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
private games.dmg.fantasystick.session.OutOfCombatRecovery outOfCombatRecovery;
|
||||
private games.dmg.fantasystick.persistence.QuickerRegenerationSettings quickerRegenerationSettings;
|
||||
private games.dmg.fantasystick.passive.QuickerRegenerationI quickerRegeneration;
|
||||
private games.dmg.fantasystick.persistence.QuickerRegenerationIISettings quickerRegenerationIISettings;
|
||||
private games.dmg.fantasystick.passive.QuickerRegenerationII quickerRegenerationII;
|
||||
private games.dmg.fantasystick.passive.PassiveRegistry passiveRegistry;
|
||||
private games.dmg.fantasystick.passive.PassiveEngine passiveEngine;
|
||||
private java.util.concurrent.CompletionStage<Void> configurationDrain = java.util.concurrent.CompletableFuture.completedStage(null);
|
||||
@@ -140,6 +142,9 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
var quickerRegenerationInitial = games.dmg.fantasystick.persistence.QuickerRegenerationSettings.parse(
|
||||
getConfig().getString(games.dmg.fantasystick.persistence.QuickerRegenerationSettings.KEY,
|
||||
games.dmg.fantasystick.persistence.QuickerRegenerationSettings.DEFAULT));
|
||||
var quickerRegenerationIIInitial = games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.parse(
|
||||
getConfig().getString(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY,
|
||||
games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.DEFAULT));
|
||||
var sheetPackDefaults = games.dmg.fantasystick.persistence.SheetResourcePackSettings.packagedDefaults();
|
||||
sheetResourcePackSettings = new games.dmg.fantasystick.persistence.SheetResourcePackSettings(getDataFolder().toPath(),
|
||||
getConfig().get(games.dmg.fantasystick.persistence.SheetResourcePackSettings.URL_KEY),
|
||||
@@ -346,13 +351,23 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
quickerRegenerationSettings = new games.dmg.fantasystick.persistence.QuickerRegenerationSettings(
|
||||
getDataFolder().toPath(), quickerRegenerationInitial.bonus().toString());
|
||||
quickerRegeneration = new games.dmg.fantasystick.passive.QuickerRegenerationI(quickerRegenerationInitial);
|
||||
quickerRegenerationIISettings = new games.dmg.fantasystick.persistence.QuickerRegenerationIISettings(
|
||||
getDataFolder().toPath(), quickerRegenerationIIInitial.bonus().toString());
|
||||
quickerRegenerationII = new games.dmg.fantasystick.passive.QuickerRegenerationII(quickerRegenerationIIInitial);
|
||||
passiveRegistry = new games.dmg.fantasystick.passive.PassiveRegistry();
|
||||
passiveRegistry.register(quickerRegeneration.definition());
|
||||
passiveRegistry.register(quickerRegenerationII.definition());
|
||||
passiveEngine = new games.dmg.fantasystick.passive.PassiveEngine(passiveRegistry, characters, sessions, nextTick,
|
||||
problem -> getLogger().log(Level.WARNING, "Passive change failed for character " + problem.character()
|
||||
+ "; passive " + problem.passive(), problem.cause()));
|
||||
outOfCombatRecovery.attachBonusHealing(record -> record.passives().contains(
|
||||
games.dmg.fantasystick.passive.QuickerRegenerationI.ID) ? quickerRegeneration.bonus() : java.math.BigInteger.ZERO);
|
||||
outOfCombatRecovery.attachBonusHealing(record -> {
|
||||
if (!record.passives().contains(games.dmg.fantasystick.passive.QuickerRegenerationI.ID)) {
|
||||
return java.math.BigInteger.ZERO;
|
||||
}
|
||||
var bonus = quickerRegeneration.bonus();
|
||||
return record.passives().contains(games.dmg.fantasystick.passive.QuickerRegenerationII.ID)
|
||||
? bonus.add(quickerRegenerationII.bonus()) : bonus;
|
||||
});
|
||||
games.dmg.fantasystick.ability.AbilityScheduler abilityScheduler = (delay, action) -> {
|
||||
long ticks = Math.max(1, (delay.toNanos() + 49_999_999L) / 50_000_000L);
|
||||
var task = getServer().getScheduler().runTaskLater(this, action, ticks);
|
||||
@@ -409,6 +424,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
admin.attachSheetResourcePack(sheetResourcePackSettings, sheetResourcePack);
|
||||
admin.attachOutOfCombatRecovery(outOfCombatRecoverySettings, outOfCombatRecovery);
|
||||
admin.attachQuickerRegeneration(quickerRegenerationSettings, quickerRegeneration, passiveRegistry);
|
||||
admin.attachQuickerRegenerationII(quickerRegenerationIISettings, quickerRegenerationII, passiveRegistry);
|
||||
admin.attachCharacterIssuer(new games.dmg.fantasystick.session.AdminCharacterIssuer(characters,
|
||||
() -> factory.create(rules), nextTick, problem -> getLogger().log(Level.WARNING,
|
||||
"Admin character issue " + problem.operation() + " for player " + problem.player()
|
||||
@@ -550,8 +566,9 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
var closingOutOfCombatRecovery = outOfCombatRecovery; outOfCombatRecovery = null;
|
||||
var closingOutOfCombatRecoverySettings = outOfCombatRecoverySettings; outOfCombatRecoverySettings = null;
|
||||
var closingQuickerRegenerationSettings = quickerRegenerationSettings; quickerRegenerationSettings = null;
|
||||
var closingQuickerRegenerationIISettings = quickerRegenerationIISettings; quickerRegenerationIISettings = null;
|
||||
var closingPassiveEngine = passiveEngine; passiveEngine = null;
|
||||
quickerRegeneration = null; passiveRegistry = null;
|
||||
quickerRegeneration = null; quickerRegenerationII = null; passiveRegistry = 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);
|
||||
@@ -590,6 +607,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
if (closingSheetResourcePack != null) cleanup("sheet resource-pack", closingSheetResourcePack::close);
|
||||
if (closingOutOfCombatRecoverySettings != null) cleanup("out-of-combat recovery configuration", closingOutOfCombatRecoverySettings::close);
|
||||
if (closingQuickerRegenerationSettings != null) cleanup("Quicker Regeneration I configuration", closingQuickerRegenerationSettings::close);
|
||||
if (closingQuickerRegenerationIISettings != null) cleanup("Quicker Regeneration II configuration", closingQuickerRegenerationIISettings::close);
|
||||
configurationDrain = java.util.concurrent.CompletableFuture.allOf(
|
||||
closingSettings == null ? java.util.concurrent.CompletableFuture.completedFuture(null)
|
||||
: closingSettings.drained().toCompletableFuture(),
|
||||
@@ -640,7 +658,9 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
closingOutOfCombatRecoverySettings == null ? java.util.concurrent.CompletableFuture.completedFuture(null)
|
||||
: closingOutOfCombatRecoverySettings.drained().toCompletableFuture(),
|
||||
closingQuickerRegenerationSettings == null ? java.util.concurrent.CompletableFuture.completedFuture(null)
|
||||
: closingQuickerRegenerationSettings.drained().toCompletableFuture());
|
||||
: closingQuickerRegenerationSettings.drained().toCompletableFuture(),
|
||||
closingQuickerRegenerationIISettings == null ? java.util.concurrent.CompletableFuture.completedFuture(null)
|
||||
: closingQuickerRegenerationIISettings.drained().toCompletableFuture());
|
||||
if (closingBooks != null) cleanup("character book", closingBooks::close);
|
||||
if (closingAdminRevival != null) cleanup("admin revival", closingAdminRevival::close);
|
||||
if (closingBasicAttack != null) cleanup("basic attack", closingBasicAttack::close);
|
||||
|
||||
@@ -158,8 +158,11 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet()));
|
||||
if (!passiveDefinitions.isEmpty()) passiveViews.put(input.session().player(), passiveView);
|
||||
var details = definitions.stream().map(definition -> abilityPages(record, definition, abilityView)).toList();
|
||||
var passiveNames = passiveDefinitions.stream().collect(java.util.stream.Collectors.toMap(
|
||||
games.dmg.fantasystick.passive.PassiveDefinition::id,
|
||||
games.dmg.fantasystick.passive.PassiveDefinition::name));
|
||||
var passiveDetails = passiveDefinitions.stream()
|
||||
.map(definition -> passiveDetailPages(record, definition, passiveView)).toList();
|
||||
.map(definition -> passiveDetailPages(record, definition, passiveNames, passiveView)).toList();
|
||||
var placeholderTargets = definitions.stream().collect(java.util.stream.Collectors.toMap(
|
||||
games.dmg.fantasystick.ability.AbilityDefinition::id, ignored -> 1));
|
||||
var passivePlaceholderTargets = passiveDefinitions.stream().collect(java.util.stream.Collectors.toMap(
|
||||
@@ -293,11 +296,12 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
}
|
||||
|
||||
private static java.util.List<Component> passiveDetailPages(CharacterRecord record,
|
||||
games.dmg.fantasystick.passive.PassiveDefinition definition, PassiveView view) {
|
||||
games.dmg.fantasystick.passive.PassiveDefinition definition, Map<String, String> names, PassiveView view) {
|
||||
String requirements = definition.requirements().entrySet().stream().sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> entry.getKey() + " " + entry.getValue()).collect(java.util.stream.Collectors.joining(", "));
|
||||
if (!definition.prerequisites().isEmpty()) {
|
||||
String prerequisiteNames = definition.prerequisites().stream().sorted().collect(java.util.stream.Collectors.joining(", "));
|
||||
String prerequisiteNames = definition.prerequisites().stream().map(id -> names.getOrDefault(id, id)).sorted()
|
||||
.collect(java.util.stream.Collectors.joining(", "));
|
||||
requirements = requirements.isEmpty() ? prerequisiteNames : prerequisiteNames + ", " + requirements;
|
||||
}
|
||||
boolean learned = record.passives().contains(definition.id());
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package games.dmg.fantasystick.passive;
|
||||
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** Quicker Regeneration II metadata and live configured pulse bonus. */
|
||||
public final class QuickerRegenerationII {
|
||||
public static final String ID = "quicker_regeneration_ii";
|
||||
public record Settings(BigInteger bonus) {
|
||||
public Settings {
|
||||
Objects.requireNonNull(bonus);
|
||||
if (bonus.signum() < 0 || bonus.compareTo(BigInteger.valueOf(1_024)) > 0) {
|
||||
throw new IllegalArgumentException("Invalid Quicker Regeneration II bonus");
|
||||
}
|
||||
}
|
||||
public static Settings defaults() { return new Settings(BigInteger.TWO); }
|
||||
}
|
||||
private volatile Settings settings;
|
||||
public QuickerRegenerationII(Settings settings) { this.settings = Objects.requireNonNull(settings); }
|
||||
public void configure(Settings value) { settings = Objects.requireNonNull(value); }
|
||||
public BigInteger bonus() { return settings.bonus(); }
|
||||
public PassiveDefinition definition() {
|
||||
return new PassiveDefinition(ID, "Quicker Regeneration II",
|
||||
Map.of(Stat.CON, BigInteger.valueOf(16), Stat.WIS, BigInteger.valueOf(14)),
|
||||
Set.of(QuickerRegenerationI.ID), BigInteger.ONE,
|
||||
List.of("Add " + bonus() + " health point" + (bonus().equals(BigInteger.ONE) ? "" : "s")
|
||||
+ " beyond Quicker Regeneration I to each eligible out-of-combat recovery pulse.",
|
||||
"Both bonuses use the same capped persisted custom-healing transaction as base recovery."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package games.dmg.fantasystick.persistence;
|
||||
|
||||
import games.dmg.fantasystick.passive.QuickerRegenerationII;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
|
||||
/** Durable save-before-publication Quicker Regeneration II balance. */
|
||||
public final class QuickerRegenerationIISettings implements AutoCloseable {
|
||||
public static final String KEY = "quicker-regeneration-ii-bonus-healing";
|
||||
public static final String DEFAULT = "2";
|
||||
private static final int MAX_BYTES = 1_048_576;
|
||||
@FunctionalInterface interface Replacement { void move(Path from, Path to) throws IOException; }
|
||||
public record Update(String value, QuickerRegenerationII.Settings settings, long sequence) { }
|
||||
private final Path directory; private final Replacement replacement;
|
||||
private final java.util.concurrent.ExecutorService worker = Executors.newSingleThreadExecutor(
|
||||
task -> new Thread(task, "fantasy-quicker-regeneration-ii-configuration"));
|
||||
private volatile String effective; private String persisted; private long persistedSequence, publishedSequence;
|
||||
private boolean closed; private int pending; private final CompletableFuture<Void> drained = new CompletableFuture<>();
|
||||
|
||||
public QuickerRegenerationIISettings(Path directory, String initial) {
|
||||
this(directory, initial, (from, to) -> Files.move(from, to,
|
||||
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING));
|
||||
}
|
||||
QuickerRegenerationIISettings(Path directory, String initial, Replacement replacement) {
|
||||
this.directory = Objects.requireNonNull(directory).toAbsolutePath(); this.replacement = Objects.requireNonNull(replacement);
|
||||
effective = canonical(initial); persisted = effective;
|
||||
}
|
||||
public static QuickerRegenerationII.Settings parse(String raw) {
|
||||
try { return new QuickerRegenerationII.Settings(new java.math.BigInteger(raw)); }
|
||||
catch (NumberFormatException invalid) { throw new IllegalArgumentException("Bonus must be a whole number", invalid); }
|
||||
}
|
||||
private static String canonical(String raw) { var parsed = parse(raw); return parsed.bonus().toString(); }
|
||||
public String value() { return effective; }
|
||||
public QuickerRegenerationII.Settings settings() { return parse(effective); }
|
||||
public CompletionStage<Update> set(String raw) {
|
||||
String accepted = canonical(raw);
|
||||
synchronized (this) {
|
||||
if (closed) return CompletableFuture.failedStage(new IOException("Configuration store is closed")); pending++;
|
||||
}
|
||||
try {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try { save(accepted); persisted = accepted; return new Update(accepted, parse(accepted), ++persistedSequence); }
|
||||
catch (IOException | RuntimeException failure) {
|
||||
throw new java.util.concurrent.CompletionException(failure instanceof IllegalArgumentException
|
||||
? failure : new IOException("Quicker Regeneration II configuration save failed; effective value unchanged", failure));
|
||||
} finally { finished(); }
|
||||
}, worker).minimalCompletionStage();
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
finished(); return CompletableFuture.failedStage(new IOException("Configuration store is closed", rejected));
|
||||
}
|
||||
}
|
||||
public synchronized boolean publish(Update update) {
|
||||
if (update.sequence() <= publishedSequence) return false;
|
||||
effective = canonical(update.value()); publishedSequence = update.sequence(); return true;
|
||||
}
|
||||
private void save(String value) throws IOException {
|
||||
Files.createDirectories(directory);
|
||||
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Unsafe directory");
|
||||
var file = directory.resolve("config.yml"); var lockFile = directory.resolve(".config.lock");
|
||||
if (Files.exists(lockFile, LinkOption.NOFOLLOW_LINKS) && !Files.isRegularFile(lockFile, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Unsafe lock");
|
||||
}
|
||||
try (var channel = FileChannel.open(lockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
|
||||
LinkOption.NOFOLLOW_LINKS); var lock = channel.tryLock()) {
|
||||
if (lock == null) throw new IOException("Configuration busy");
|
||||
var options = new LoaderOptions(); options.setAllowDuplicateKeys(false); options.setMaxAliasesForCollections(20);
|
||||
options.setCodePointLimit(MAX_BYTES); var yaml = new Yaml(new SafeConstructor(options));
|
||||
Map<String, Object> document = new LinkedHashMap<>();
|
||||
if (Files.exists(file, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Unsafe configuration");
|
||||
byte[] bytes; try (var input = Files.newInputStream(file, LinkOption.NOFOLLOW_LINKS)) { bytes = input.readNBytes(MAX_BYTES + 1); }
|
||||
if (bytes.length > MAX_BYTES) throw new IOException("Configuration too large");
|
||||
Object loaded = yaml.load(StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString());
|
||||
if (loaded != null) {
|
||||
if (!(loaded instanceof Map<?, ?> map)) throw new IOException("Invalid configuration root");
|
||||
for (var entry : map.entrySet()) {
|
||||
if (!(entry.getKey() instanceof String key)) throw new IOException("Invalid configuration key");
|
||||
document.put(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
document.put(KEY, value); byte[] bytes = yaml.dump(document).getBytes(StandardCharsets.UTF_8);
|
||||
Path temporary = Files.createTempFile(directory, ".config-", ".tmp");
|
||||
try {
|
||||
try (var output = FileChannel.open(temporary, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
|
||||
var buffer = ByteBuffer.wrap(bytes); while (buffer.hasRemaining()) output.write(buffer); output.force(true);
|
||||
}
|
||||
replacement.move(temporary, file);
|
||||
} finally { Files.deleteIfExists(temporary); }
|
||||
} catch (OverlappingFileLockException busy) { throw new IOException("Configuration busy", busy); }
|
||||
}
|
||||
private synchronized void finished() { pending--; if (closed && pending == 0) drained.complete(null); }
|
||||
public CompletionStage<Void> drained() { return drained.minimalCompletionStage(); }
|
||||
@Override public synchronized void close() { closed = true; worker.shutdown(); if (pending == 0) drained.complete(null); }
|
||||
}
|
||||
@@ -70,6 +70,8 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
private OutOfCombatRecovery outOfCombatRecovery;
|
||||
private games.dmg.fantasystick.persistence.QuickerRegenerationSettings quickerRegenerationSettings;
|
||||
private games.dmg.fantasystick.passive.QuickerRegenerationI quickerRegeneration;
|
||||
private games.dmg.fantasystick.persistence.QuickerRegenerationIISettings quickerRegenerationIISettings;
|
||||
private games.dmg.fantasystick.passive.QuickerRegenerationII quickerRegenerationII;
|
||||
private games.dmg.fantasystick.passive.PassiveRegistry passiveRegistry;
|
||||
|
||||
public EquipmentAdminCommand(EquipmentSessionStore receipts, EquipmentSessions sessions, Server server,
|
||||
@@ -313,6 +315,14 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
passiveRegistry = Objects.requireNonNull(registry);
|
||||
}
|
||||
|
||||
public void attachQuickerRegenerationII(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings settings,
|
||||
games.dmg.fantasystick.passive.QuickerRegenerationII passive,
|
||||
games.dmg.fantasystick.passive.PassiveRegistry registry) {
|
||||
if (closed || quickerRegenerationIISettings != null) throw new IllegalStateException("Quicker Regeneration II already attached or command closed");
|
||||
quickerRegenerationIISettings = Objects.requireNonNull(settings); quickerRegenerationII = Objects.requireNonNull(passive);
|
||||
if (passiveRegistry != Objects.requireNonNull(registry)) throw new IllegalArgumentException("Passive registry mismatch");
|
||||
}
|
||||
|
||||
public void attachExperienceGrant(AdminExperienceGrant grant) {
|
||||
if (closed || experienceGrant != null) throw new IllegalStateException("Experience grant already attached or command closed");
|
||||
experienceGrant = Objects.requireNonNull(grant);
|
||||
@@ -498,6 +508,10 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
&& quickerRegenerationSettings != null) {
|
||||
quickerRegenerationConfiguration(sender, args); return;
|
||||
}
|
||||
if (args[1].equals(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY)
|
||||
&& quickerRegenerationIISettings != null) {
|
||||
quickerRegenerationIIConfiguration(sender, args); return;
|
||||
}
|
||||
if (longbowSettings != null && games.dmg.fantasystick.persistence.LongbowSettings.keys().contains(args[1])) {
|
||||
longbowConfiguration(sender, args); return;
|
||||
}
|
||||
@@ -642,6 +656,35 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
});
|
||||
}
|
||||
|
||||
private void quickerRegenerationIIConfiguration(CommandSender sender, String[] args) {
|
||||
if (args.length == 2) {
|
||||
sender.sendMessage(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY + "="
|
||||
+ quickerRegenerationIISettings.value() + " (applies to future recovery pulses only)"); return;
|
||||
}
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.Update> operation;
|
||||
try { operation = quickerRegenerationIISettings.set(args[2]); }
|
||||
catch (IllegalArgumentException invalid) {
|
||||
sender.sendMessage("Quicker Regeneration II setting is invalid; the effective value is unchanged."); return;
|
||||
}
|
||||
operation.whenComplete((saved, failure) -> {
|
||||
if (failure != null) report(null, "config-quicker-regeneration-ii", failure); if (closed) return;
|
||||
try { nextTick.accept(() -> {
|
||||
if (closed) return; boolean applied = false;
|
||||
if (failure == null) {
|
||||
applied = quickerRegenerationIISettings.publish(saved);
|
||||
if (applied) { quickerRegenerationII.configure(saved.settings()); passiveRegistry.replace(quickerRegenerationII.definition()); }
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
if (failure != null) sender.sendMessage("Quicker Regeneration II configuration save failed; effective value is unchanged.");
|
||||
else if (!applied) sender.sendMessage("Saved request was superseded; effective "
|
||||
+ games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY + "=" + quickerRegenerationIISettings.value() + ".");
|
||||
else sender.sendMessage("Saved " + games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY
|
||||
+ "=" + saved.value() + " for future recovery pulses.");
|
||||
}); } catch (RuntimeException rejected) { report(null, "config-response", rejected); }
|
||||
});
|
||||
}
|
||||
|
||||
private void longbowConfiguration(CommandSender sender, String[] args) {
|
||||
String key = args[1];
|
||||
if (args.length == 2) {
|
||||
@@ -1071,6 +1114,7 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
var keys = new java.util.ArrayList<String>(); keys.add("adventure-mode");keys.add(games.dmg.fantasystick.persistence.AdminReviveSettings.KEY);keys.add(games.dmg.fantasystick.persistence.SwitchDelaySettings.KEY);keys.add(games.dmg.fantasystick.persistence.RoleplaySettings.KEY);keys.add(games.dmg.fantasystick.persistence.PartySettings.KEY);keys.add(games.dmg.fantasystick.persistence.SheetResourcePackSettings.URL_KEY);keys.add(games.dmg.fantasystick.persistence.SheetResourcePackSettings.SHA1_KEY);
|
||||
keys.addAll(games.dmg.fantasystick.persistence.OutOfCombatRecoverySettings.keys());
|
||||
keys.add(games.dmg.fantasystick.persistence.QuickerRegenerationSettings.KEY);
|
||||
keys.add(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY);
|
||||
keys.addAll(games.dmg.fantasystick.persistence.FrostShardSettings.keys());
|
||||
keys.addAll(games.dmg.fantasystick.persistence.LongbowSettings.keys());
|
||||
keys.addAll(games.dmg.fantasystick.persistence.QuickstepSettings.keys());
|
||||
@@ -1109,6 +1153,12 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
return matching(current.equals(games.dmg.fantasystick.persistence.QuickerRegenerationSettings.DEFAULT)
|
||||
? List.of(current) : List.of(current, games.dmg.fantasystick.persistence.QuickerRegenerationSettings.DEFAULT), args[2]);
|
||||
}
|
||||
if (args[1].equals(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.KEY)
|
||||
&& quickerRegenerationIISettings != null) {
|
||||
String current = quickerRegenerationIISettings.value();
|
||||
return matching(current.equals(games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.DEFAULT)
|
||||
? List.of(current) : List.of(current, games.dmg.fantasystick.persistence.QuickerRegenerationIISettings.DEFAULT), args[2]);
|
||||
}
|
||||
if (frostSettings != null && games.dmg.fantasystick.persistence.FrostShardSettings.keys().contains(args[1])) {
|
||||
String current = frostSettings.value(args[1]);
|
||||
String defaults = games.dmg.fantasystick.persistence.FrostShardSettings.defaultValue(args[1]);
|
||||
|
||||
@@ -16,6 +16,7 @@ out-of-combat-healing-delay-seconds: 30
|
||||
out-of-combat-healing-interval-seconds: 10
|
||||
out-of-combat-healing-amount: 1
|
||||
quicker-regeneration-i-bonus-healing: 1
|
||||
quicker-regeneration-ii-bonus-healing: 2
|
||||
|
||||
# New-character and transferable fantasy progression defaults.
|
||||
starting-points: 40
|
||||
|
||||
@@ -130,6 +130,14 @@ class OutOfCombatRecoveryNativeTest {
|
||||
assertTrue(full.events.isEmpty()); assertEquals(fullRevision, full.record.get().revision());
|
||||
}
|
||||
|
||||
@Test void stackedRegenerationBonusProducesOneMaximumCappedNativeAndPersistedPulse() {
|
||||
var f = new Direct(); f.health.set(9d); f.record.set(f.saved(BigDecimal.valueOf(9)));
|
||||
f.recovery.attachBonusHealing(ignored -> BigInteger.valueOf(3));
|
||||
f.recovery.sweep(); f.clock.advance(Duration.ofSeconds(30)); f.recovery.sweep();
|
||||
assertEquals(1, f.events.size()); assertEquals(1, f.events.getFirst().getAmount());
|
||||
f.runNext(); assertEquals(10, f.health.get()); assertEquals(0, BigDecimal.TEN.compareTo(f.record.get().health()));
|
||||
}
|
||||
|
||||
@Test void zeroAmountDisablesPulsesAndReconfigurationRetainsTheActivityTimestamp() {
|
||||
var f = new Direct(new OutOfCombatRecovery.Settings(Duration.ofSeconds(30), Duration.ofSeconds(10), BigInteger.ZERO));
|
||||
f.recovery.sweep(); f.clock.advance(Duration.ofSeconds(40)); f.recovery.sweep();
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import games.dmg.fantasystick.persistence.CharacterRecord;
|
||||
import games.dmg.fantasystick.persistence.CharacterStorage;
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import games.dmg.fantasystick.session.OutOfCombatRecovery;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class QuickerRegenerationIINativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void prerequisiteLearnStackTransferTailReviveAndDependencySafeRefundUseTwoPassiveSlots() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"character-swap-cooldown-seconds: 0\nout-of-combat-healing-delay-seconds: 0\n"
|
||||
+ "out-of-combat-healing-interval-seconds: 10\nout-of-combat-healing-amount: 1\n"
|
||||
+ "quicker-regeneration-i-bonus-healing: 1\nquicker-regeneration-ii-bonus-healing: 2\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
makeEligible(data, a, BigDecimal.valueOf(4)); f.players.put(a.core.id, a.player); f.activate(a); PartyBookNativeTest.open(f, a);
|
||||
String initial = PartyBookNativeTest.text(a).replaceAll("\\s+", "");
|
||||
assertTrue(initial.contains("QuickerRegenerationIIQuickerRegenerationI,CON16,WIS14"), initial);
|
||||
assertFalse(hasAction(a, "[Learn]", "quicker_regeneration_ii"));
|
||||
|
||||
PartyBookNativeTest.click(f, a, action(a, "[Learn]", "quicker_regeneration_i")); drain(f);
|
||||
PartyBookNativeTest.click(f, a, action(a, "[Learn]", "quicker_regeneration_ii")); drain(f);
|
||||
var learned = storage(f).find(a.core.record.id()).get().orElseThrow();
|
||||
assertEquals(List.of("quicker_regeneration_i", "quicker_regeneration_ii", ""), learned.passives());
|
||||
assertEquals(BigInteger.valueOf(38), learned.unspentPoints());
|
||||
assertEquals(a.core.record.loadout(), learned.loadout());
|
||||
|
||||
var recovery = (OutOfCombatRecovery) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "outOfCombatRecovery");
|
||||
recovery.sweep(); PartyBookNativeTest.drain(f);
|
||||
assertEquals(8, a.player.getHealth(), "The single pulse is base 1 + Regeneration I 1 + Regeneration II 2");
|
||||
learned = storage(f).find(learned.id()).get().orElseThrow(); assertEquals(0, new BigDecimal("8").compareTo(learned.health()));
|
||||
|
||||
PartyBookNativeTest.click(f, a, action(a, "[Unlearn]", "quicker_regeneration_i")); drain(f);
|
||||
var refused = storage(f).find(learned.id()).get().orElseThrow();
|
||||
assertEquals(learned.passives(), refused.passives()); assertEquals(learned.unspentPoints(), refused.unspentPoints());
|
||||
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a); PartyBookNativeTest.drain(f); f.awaitCharacterAndReceipt(a);
|
||||
var b = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
b.core.inventory.setItemInOffHand(new games.dmg.fantasystick.items.CharacterSheets().create(refused));
|
||||
f.players.put(b.core.id, b.player); f.activate(b); PartyBookNativeTest.open(f, b);
|
||||
assertTrue(PartyBookNativeTest.text(b).replaceAll("\\s+", "").contains("2:QuickerRegenerationII"));
|
||||
|
||||
b.core.inventory.setItemInOffHand(null); f.changed(b); PartyBookNativeTest.drain(f); f.awaitCharacterAndReceipt(b);
|
||||
var exited = storage(f).find(refused.id()).get().orElseThrow();
|
||||
var dead = storage(f).recordDeath(exited.id(), exited.revision()).get().orElseThrow();
|
||||
assertEquals(refused.passives(), dead.passives());
|
||||
var revived = storage(f).restoreDeathRecordAdministratively(dead.id(), dead.deathId(), new BigDecimal("1024")).get().orElseThrow();
|
||||
assertEquals(refused.passives(), revived.passives());
|
||||
var c = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
c.core.inventory.setItemInOffHand(new games.dmg.fantasystick.items.CharacterSheets().create(revived));
|
||||
f.players.put(c.core.id, c.player); f.activate(c); PartyBookNativeTest.open(f, c);
|
||||
PartyBookNativeTest.click(f, c, action(c, "[Unlearn]", "quicker_regeneration_ii")); drain(f);
|
||||
PartyBookNativeTest.click(f, c, action(c, "[Unlearn]", "quicker_regeneration_i")); drain(f);
|
||||
var refunded = storage(f).find(revived.id()).get().orElseThrow();
|
||||
assertEquals(List.of("", "", ""), refunded.passives()); assertEquals(BigInteger.valueOf(40), refunded.unspentPoints());
|
||||
f.plugin.setEnabled(false); f.plugin.setEnabled(true);
|
||||
assertEquals(refunded.passives(), storage(f).find(refunded.id()).get().orElseThrow().passives());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void orphanedSavedSecondRankRemainsVisibleButCannotApplyWithoutItsPrerequisite() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"out-of-combat-healing-delay-seconds: 0\nout-of-combat-healing-interval-seconds: 10\n"
|
||||
+ "out-of-combat-healing-amount: 1\nquicker-regeneration-i-bonus-healing: 1\n"
|
||||
+ "quicker-regeneration-ii-bonus-healing: 2\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
makeEligible(data, a, BigDecimal.valueOf(4), List.of("quicker_regeneration_ii", "", ""));
|
||||
f.players.put(a.core.id, a.player); f.activate(a); PartyBookNativeTest.open(f, a);
|
||||
assertTrue(PartyBookNativeTest.text(a).replaceAll("\\s+", "").contains("1:QuickerRegenerationII"));
|
||||
var recovery = (OutOfCombatRecovery) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "outOfCombatRecovery");
|
||||
recovery.sweep(); PartyBookNativeTest.drain(f);
|
||||
assertEquals(5, a.player.getHealth(), "An orphaned second rank cannot execute without Regeneration I");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void configurationIsPermissionAwareDurableAndPrefixFiltered() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var sender = new EquipmentAdminCommandNativeTest.Sender();
|
||||
assertEquals(List.of("quicker-regeneration-ii-bonus-healing"),
|
||||
f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "quicker-regeneration-ii"}));
|
||||
assertEquals(List.of("2"), f.command.tabComplete(sender.port, "fantasystick",
|
||||
new String[]{"config", "quicker-regeneration-ii-bonus-healing", ""}));
|
||||
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quicker-regeneration-ii-bonus-healing"});
|
||||
assertTrue(sender.messages.getLast().contains("=2")); sender.allowed = false;
|
||||
assertTrue(f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "quicker-regeneration-ii"}).isEmpty());
|
||||
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quicker-regeneration-ii-bonus-healing", "5"});
|
||||
assertTrue(sender.messages.getLast().contains("Permission denied")); sender.allowed = true;
|
||||
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quicker-regeneration-ii-bonus-healing", "5"});
|
||||
var callback = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(callback); callback.run();
|
||||
f.plugin.setEnabled(false); f.plugin.setEnabled(true);
|
||||
var settings = (games.dmg.fantasystick.persistence.QuickerRegenerationIISettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "quickerRegenerationIISettings");
|
||||
assertEquals("5", settings.value());
|
||||
}
|
||||
}
|
||||
|
||||
private static void makeEligible(Path data, FantasyStickLifecycleNativeTest.Actor actor, BigDecimal health) throws Exception {
|
||||
makeEligible(data, actor, health, List.of("", "", ""));
|
||||
}
|
||||
private static void makeEligible(Path data, FantasyStickLifecycleNativeTest.Actor actor, BigDecimal health,
|
||||
List<String> passives) throws Exception {
|
||||
var repository = new games.dmg.fantasystick.persistence.YamlCharacterRepository(data);
|
||||
var old = repository.load(actor.core.record.id()).orElseThrow(); var stats = new EnumMap<>(old.stats());
|
||||
stats.put(Stat.CON, BigInteger.valueOf(16)); stats.put(Stat.WIS, BigInteger.valueOf(14));
|
||||
repository.replace(new CharacterRecord(old.id(), old.name(), old.revision().add(BigInteger.ONE), stats,
|
||||
old.level(), old.experience(), BigInteger.valueOf(40), health, old.learned(), old.loadout(), passives,
|
||||
old.cooldowns(), old.restrictedKills(), false, null, null), old.revision());
|
||||
}
|
||||
private static void drain(FantasyStickLifecycleNativeTest.Fixture f) throws Exception {
|
||||
PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
}
|
||||
private static CharacterStorage storage(FantasyStickLifecycleNativeTest.Fixture f) {
|
||||
return (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
}
|
||||
private static boolean hasAction(FantasyStickLifecycleNativeTest.Actor actor, String label, String passive) {
|
||||
try { action(actor, label, passive); return true; } catch (AssertionError missing) { return false; }
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom action(
|
||||
FantasyStickLifecycleNativeTest.Actor actor, String label, String passive) {
|
||||
var content = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(actor.books.getLast())
|
||||
.get(net.minecraft.core.component.DataComponents.WRITTEN_BOOK_CONTENT);
|
||||
assertNotNull(content);
|
||||
for (var page : content.pages()) { var found = action(page.raw(), label, passive); if (found != null) return found; }
|
||||
throw new AssertionError("Missing " + label + " for " + passive);
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom action(
|
||||
net.minecraft.network.chat.Component component, String label, String passive) {
|
||||
if (component.getString().equals(label)
|
||||
&& component.getStyle().getClickEvent() instanceof net.minecraft.network.chat.ClickEvent.Custom custom
|
||||
&& custom.payload().orElse(null) instanceof net.minecraft.nbt.StringTag payload
|
||||
&& payload.value().contains(":" + passive)) return custom;
|
||||
for (var child : component.getSiblings()) { var found = action(child, label, passive); if (found != null) return found; }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class QuickerRegenerationNativeTest {
|
||||
@Test void liveBonusConfigurationIsPermissionAwareDurableAndFuturePulseOnly() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var sender = new EquipmentAdminCommandNativeTest.Sender();
|
||||
assertEquals(List.of("quicker-regeneration-i-bonus-healing"),
|
||||
assertEquals(List.of("quicker-regeneration-i-bonus-healing", "quicker-regeneration-ii-bonus-healing"),
|
||||
f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "quicker"}));
|
||||
assertEquals(List.of("1"), f.command.tabComplete(sender.port, "fantasystick",
|
||||
new String[]{"config", "quicker-regeneration-i-bonus-healing", ""}));
|
||||
|
||||
@@ -88,7 +88,7 @@ class DistributionTest {
|
||||
assertTrue(defaults.contains("sheet-resource-pack-sha1: ''"));
|
||||
for (String setting : java.util.List.of("out-of-combat-healing-delay-seconds: 30",
|
||||
"out-of-combat-healing-interval-seconds: 10", "out-of-combat-healing-amount: 1",
|
||||
"quicker-regeneration-i-bonus-healing: 1")) {
|
||||
"quicker-regeneration-i-bonus-healing: 1", "quicker-regeneration-ii-bonus-healing: 2")) {
|
||||
assertTrue(defaults.contains(setting), "Missing packaged recovery default: " + setting);
|
||||
}
|
||||
assertTrue(JavaPlugin.class.isAssignableFrom(FantasyStickPlugin.class));
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package games.dmg.fantasystick.persistence;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import games.dmg.fantasystick.passive.QuickerRegenerationI;
|
||||
import games.dmg.fantasystick.passive.QuickerRegenerationII;
|
||||
import games.dmg.fantasystick.progression.ProgressionRules;
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class QuickerRegenerationIIPassiveTest {
|
||||
@TempDir Path data;
|
||||
|
||||
@Test void secondRankRequiresFirstRankAndRefusesAFullPassiveLoadoutBeforeSpending() throws Exception {
|
||||
var base = new games.dmg.fantasystick.crafting.CharacterFactory().create(ProgressionRules.defaults());
|
||||
var stats = new EnumMap<>(base.stats()); stats.put(Stat.CON, BigInteger.valueOf(16)); stats.put(Stat.WIS, BigInteger.valueOf(14));
|
||||
var noFirst = record(base, stats, List.of("", "", ""));
|
||||
var repository = new YamlCharacterRepository(data); repository.create(noFirst);
|
||||
try (var storage = new CharacterStorage(repository, Clock.systemUTC())) {
|
||||
var requirements = Map.of(Stat.CON, BigInteger.valueOf(16), Stat.WIS, BigInteger.valueOf(14));
|
||||
assertEquals(CharacterStorage.PassiveMutationStatus.REQUIREMENTS,
|
||||
storage.unlockPassive(noFirst.id(), noFirst.revision(), QuickerRegenerationII.ID, requirements,
|
||||
Set.of(QuickerRegenerationI.ID), BigInteger.ONE).get().status());
|
||||
var withFirst = new CharacterRecord(noFirst.id(), noFirst.name(), noFirst.revision().add(BigInteger.ONE), stats,
|
||||
noFirst.level(), noFirst.experience(), noFirst.unspentPoints(), noFirst.health(), noFirst.learned(), noFirst.loadout(),
|
||||
List.of(QuickerRegenerationI.ID, "unknown_a", "unknown_b"), noFirst.cooldowns(), noFirst.restrictedKills(),
|
||||
false, null, null);
|
||||
repository.replace(withFirst, noFirst.revision());
|
||||
var full = storage.unlockPassive(withFirst.id(), withFirst.revision(), QuickerRegenerationII.ID, requirements,
|
||||
Set.of(QuickerRegenerationI.ID), BigInteger.ONE).get();
|
||||
assertEquals(CharacterStorage.PassiveMutationStatus.PASSIVE_SLOTS_FULL, full.status());
|
||||
assertEquals(withFirst, repository.load(withFirst.id()).orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
private static CharacterRecord record(CharacterRecord base, EnumMap<Stat, BigInteger> stats, List<String> passives) {
|
||||
return new CharacterRecord(base.id(), base.name(), base.revision(), stats, base.level(), base.experience(),
|
||||
BigInteger.valueOf(40), BigDecimal.valueOf(10), base.learned(), base.loadout(), passives, base.cooldowns(),
|
||||
base.restrictedKills(), false, null, null);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package games.dmg.fantasystick.persistence;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class QuickerRegenerationIISettingsTest {
|
||||
@TempDir Path data;
|
||||
|
||||
@Test void validatesBoundsAndPersistsBeforePublicationWithoutReplacingOtherKeys() throws Exception {
|
||||
assertEquals(java.math.BigInteger.TWO, QuickerRegenerationIISettings.parse("2").bonus());
|
||||
assertEquals(java.math.BigInteger.ZERO, QuickerRegenerationIISettings.parse("0").bonus());
|
||||
assertEquals(java.math.BigInteger.valueOf(1024), QuickerRegenerationIISettings.parse("1024").bonus());
|
||||
for (String invalid : java.util.List.of("-1", "1025", "2.5", "x")) {
|
||||
assertThrows(IllegalArgumentException.class, () -> QuickerRegenerationIISettings.parse(invalid));
|
||||
}
|
||||
Files.writeString(data.resolve("config.yml"), "quicker-regeneration-i-bonus-healing: '7'\nunrelated: retained\n");
|
||||
try (var settings = new QuickerRegenerationIISettings(data, "2")) {
|
||||
var update = settings.set("5").toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
assertEquals("2", settings.value()); assertTrue(settings.publish(update)); assertEquals("5", settings.value());
|
||||
var saved = (Map<?, ?>) new org.yaml.snakeyaml.Yaml().load(Files.readString(data.resolve("config.yml")));
|
||||
assertEquals("7", String.valueOf(saved.get("quicker-regeneration-i-bonus-healing")));
|
||||
assertEquals("retained", saved.get("unrelated"));
|
||||
assertEquals("5", String.valueOf(saved.get(QuickerRegenerationIISettings.KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void failedWriteAndCloseRetainThePriorEffectiveValue() throws Exception {
|
||||
try (var settings = new QuickerRegenerationIISettings(data, "2", (from, to) -> {
|
||||
throw new java.io.IOException("simulated");
|
||||
})) {
|
||||
assertThrows(java.util.concurrent.ExecutionException.class,
|
||||
() -> settings.set("5").toCompletableFuture().get(5, TimeUnit.SECONDS));
|
||||
assertEquals("2", settings.value());
|
||||
}
|
||||
var closed = new QuickerRegenerationIISettings(data.resolve("closed"), "2"); closed.close();
|
||||
assertThrows(java.util.concurrent.ExecutionException.class,
|
||||
() -> closed.set("5").toCompletableFuture().get(5, TimeUnit.SECONDS));
|
||||
closed.drained().toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user