Repository navigation
Dynamic Values
Pétrus Pradella edited this page Nov 2, 2026
·
3 revisions
Two numeric engines of br.com.finalcraft.evernifecore.logic.value, both kept per plugin in the plugin's own storage:
- a
DynamicValuemoves with use and drifts back - a price that rises with each sale, a pay rate that cools down; - a
Contributionis a goal several players fill together - a clan warehouse, a town's donation target.
DynamicValues values = DynamicValues.of(getPluginData(), storage); // or DynamicValues.of(plugin): memory only
DynamicValue price = values.define("diamond-price", DynamicValueSpec.base(100)
.min(new BigDecimal("50"))
.max(new BigDecimal("400"))
.step(new BigDecimal("5")) // each bump
.decay(new BigDecimal("1"), Duration.ofMinutes(10)) // back toward the base, 1 every 10 minutes
.build());
ProgressOwner market = ProgressOwner.global();
BigDecimal now = price.get(market);
price.bump(market); // +step, never past max
price.bump(market, new BigDecimal("-20")); // any delta, never past min
price.set(market, new BigDecimal("120"));
price.reset(market); // back to the base-
One state per
ProgressOwner- global, per player, per group, per region. - The decay is applied on read, from the stored instant and
ECClock: no sweep, no scheduler; it pulls toward the base without passing it. -
DynamicValueSpec.spec(NumericSpec)rounds every value;clamp(value)keeps it inmin/max. - A dynamic value is a variable:
{myplugin:diamond-price}reads (andsetwrites) the context owner's value in any Logic definition, a price included. - Storage: collection
ec_dynamic_values, key<id>@<owner>.define(id, spec)acceptsnameor<plugin>:name(another plugin's id is refused);find(id)looks one up.DynamicValues.closeOwnedBy(owner)runs at shutdown.DynamicValues.of(owner)without a storage returns the open instance, or a memory-only one when none is open; reopening with another storage waits for the writes of the instance it replaces (so doContributions.ofandBoosterManager.of). - In a config a spec is written
{base: 100, min: 50, max: 400, step: 5, decay: {amount: 1, every: 10m}}(codec registered by the core).
Any thread; the writes of one owner are serialized, and the storage ends with the owner's latest state whatever order the backend completes writes in (one write per row on its way; a change meanwhile writes again once it lands).
Contributions goals = Contributions.of(getPluginData(), storage);
Contribution<Currency> fund = goals.define("town-hall", ProgressOwner.of("group", clanId),
CurrencyRequirement.of(Currencies.money(), new BigDecimal("10000")));
fund.onComplete(Effects.parse(getPluginData(), node, zone));
DepositResult result = fund.deposit(ExecutionContext.builder(caller).player(player).build());
result.getCounted(); // what this deposit added
result.getTotal(); // the total so far
result.isCompleted(); // this deposit reached the target- Each
deposit(ctx)takes from the context's player what is still missing, by member (getByMember()), up to the target (getTotal(),isComplete()). Reaching it applies theonCompleteeffects once, with the completing deposit's context. - Requirements:
CurrencyRequirement.of(currency, amount)(from the player's own wallet) andItemRequirement.of(ItemMatcher, count)(from the player's inventory). Both consume by default;.check()only counts what the member holds, and counts only what they hold beyond what already counted. - Events:
ContributionDepositEventper deposit (getAmount(),getTotal()),ContributionCompletedEventonce. Each deposit is recorded asDEPOSITin the owner's audit log, when it has one. - Storage: collection
ec_contributions, key<id>@<target>; what a deposit writes is a snapshot of the row's latest state, one write per row on its way, so two deposits whose writes complete out of order still leave the last total in storage. -
Threads. Any thread. The requirement (
take/countHeld) runs while the contribution is not locked: a wallet or an inventory may wait for the player's thread, and that thread may be readinggetTotal()for a menu. Deposits racing for the last of the target are counted one at a time, and the total never passes the target: what one took past it goes back throughContributionRequirement.giveBack(ctx, amount)- a currency to the wallet, items to the inventory and what does not fit as a delivery. AgiveBackthat throws is auditedREFUND_FAILEDand logged severe on the plugin's log, never thrown out ofdeposit. - A custom
ContributionRequirementimplementsgetSubject,getTarget,isConsuming,countHeld(ctx),take(ctx, upTo)(what actually left, up toupTo) andgiveBack(ctx, amount).
LogicWorld.dynamicValues() and LogicWorld.contributions() give the fake plugin's managers over an in-memory
storage, closed with the world, sharing its test clock (advance(Duration) moves the decay). See
Testing Toolkit.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Block Data (SVWorldDataManager)
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Logging & Debug Modules
- Events
- Regions
- Groups
- Notifications
- NPCs
- Logic
- Quests
- Provenance
- Scheduler & Threading
- Items & NBT
- Screens
- Dialogues
- Menus
- Integrations
- Economy
- Boosters
- Deliveries
- Audit Log
- Dynamic Values
- Player Ignore
- Version Compatibility
Architecture & Reference