Astral Realms Documentation Help

Seasons & Categories

Every quest AstralFarmPass generates belongs to a category (categories.yml) and a season (SeasonEntity) — the category decides how often quests of that kind regenerate and how many points completing one is worth, and the season is the one-month container that all of a given month's quests are generated into. SeasonRotationService owns both: it rotates seasons in and out automatically, and it is the only place categories.yml is actually read to drive quest generation. See Overview for how this fits into the wider plugin, and Quests & Goals for the blueprint side of quest generation.

Categories (categories.yml)

categories.yml is loaded into a CategoriesConfiguration — a map of category id → QuestCategory:

Field

Type

Description

id

String

The category id (should match its map key).

duration

day \|week \|month

The nominal cadence of the category.

rewarded-points

int

Points credited to a player's season points each time they complete a quest of this category.

regeneration.duration

day \|week \|month

The nominal regeneration cadence.

regeneration.count

int

How many new quest instances to generate per regeneration period.

regeneration.expire-old

boolean

Whether previously generated quests of this category are meant to expire when a new period starts.

categories: daily: id: "daily" duration: "day" rewarded-points: 10 regeneration: duration: "day" # Generate 1 new daily quest each day count: 1 # Expire old daily quests at the end of the day expire-old: true weekly: id: "weekly" duration: "month" rewarded-points: 50 regeneration: duration: "week" # Generate 1 new weekly quest each week count: 1 # Do not expire old weekly quests expire-old: false

Shipped categories

Category

duration

rewarded-points

regeneration.duration

regeneration.count

regeneration.expire-old

daily

day

10

day

1

true

weekly

month

50

week

1

false

Season lifecycle

A season is a SeasonEntityuniqueId, startsAt, endsAt — persisted in the seasons table (see Configuration). Seasons are always calendar-month windows: SeasonRotationService.createNewSeason() takes YearMonth.now() and sets startsAt/endsAt from SeasonTimeUtils.getMonthStart/getMonthEnd — midnight on the 1st through 23:59:59 on the last day of the month, in the server's system default time zone.

SeasonRepository.findCurrentSeason() selects the row where starts_at <= now AND ends_at >= now LIMIT 1. Rotation only ever creates a new season when this query comes back empty or the cached season's endsAt has passed, so in practice exactly one season is active at a time.

SeasonService caches the current season in an AtomicReference<SeasonEntity>. getCurrentSeason() returns the cached value if it isn't expired, otherwise re-queries findCurrentSeason() and re-caches the result. setCurrentSeason(season) also eagerly preloads that season's quests via QuestService.findBySeason, logging how many were loaded.

Automatic rotation

SeasonRotationService starts a repeating async task in its constructor — Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::checkAndRotateSeasonIfNeeded, 0L, 6000L) — so rotation is checked immediately on startup and then every 6000 ticks (5 minutes).

Each check calls seasonService.getCurrentSeason() and branches:

Situation

Master node (config.ymlmaster: true)

Non-master node

No current season

Calls createNewSeason(), then seasonService.setCurrentSeason(season).

Logs "waiting for master to create one" and clears its cache (setCurrentSeason(null)).

Current season's endsAt has passed

Calls createNewSeason(), then seasonService.setCurrentSeason(season).

Logs "waiting for master to rotate" and clears its cache (setCurrentSeason(null)).

Current season still valid

No action.

No action.

createNewSeason():

  1. Builds a SeasonEntity for the current month (SeasonTimeUtils.getMonthStart/getMonthEnd).

  2. Saves it via SeasonRepository.save.

  3. Calls generateSeasonQuests to populate it with daily and weekly quests.

  4. Broadcasts a FarmpassUpdatedPacket (channel farmpass, packet id 0x00) over MessagingService so every other node — including the master itself — can pick up the new season. See Master vs. non-master nodes.

Quest generation

generateSeasonQuests(season) runs two independent generation passes in parallel and waits for both to finish:

Daily — for every calendar day of the season's month (1..daysInMonth), generate daily.regeneration.count quests (1 by default) with unlocksAt at the start of that day and expiresAt at the end of that day (23:59:59).

Weekly — the season's month is split into SeasonTimeUtils.getWeeksInMonth(yearMonth) weeks, using a fixed day-based scheme rather than calendar weeks: week 1 starts on day 1, week 2 on day 8, week 3 on day 15, and so on (⌈daysInMonth / 7⌉ weeks total — 5 for a 31-day month). For each week, generate weekly.regeneration.count quests (1 by default) with unlocksAt at the start of that week and expiresAt fixed to the season's endsAt (end of month) — weekly quests never expire mid-season.

Each individual quest is produced by QuestGenerationService.generateRandomQuest(seasonId, categoryId, unlocksAt, expiresAt):

  1. Collects every QuestBlueprint whose requirements map contains the given categoryId — i.e. every blueprint that declares a min/max range for that category. Fails the future if none exist.

  2. Tracks blueprints already used for this season and category in an in-memory Map<UUID seasonId, Map<String categoryId, Set<String blueprintId>>>. Prefers picking a blueprint that hasn't been used yet this season/category; once every eligible blueprint has been used at least once, the tracking set for that season/category is cleared and reuse resumes.

  3. Picks uniformly at random (java.util.Random) from the eligible pool, then rolls a random target inside that blueprint's minmax range for the category (random.nextInt(max - min + 1) + min).

  4. Saves a new QuestEntity (id, seasonId, blueprintId, the rolled value, unlocksAt, expiresAt, createdAt) to the quests table.

No-duplicate tracking is purely in-memory and per season id — it is reset by resetSeasonTracking on reroll, and is naturally scoped to a single season's lifetime since a new season gets a new random UUID. See Quests & Goals for the blueprint YAML format and how quest instances are matched to gameplay events.

Master vs. non-master nodes

Season creation and rotation are gated on config.ymlmaster (see Configuration); exactly one node in the network should run with master: true.

  • Master: its SeasonRotationService is the only one that calls createNewSeason() — both from the periodic rotation check and from /pass reroll. After creating or rerolling a season it broadcasts FarmpassUpdatedPacket.

  • Non-master: never calls createNewSeason(). When its rotation check finds no season or an expired one, it logs that it's waiting and clears its cached season (seasonService.setCurrentSeason(null)) rather than serving stale data. It picks the new season back up via messaging: SeasonService's constructor registers an exchange listener on the farmpass channel that — only when plugin.configuration().master() is false and the packet is a FarmpassUpdatedPacket — re-reads findCurrentSeason() from the database and calls setCurrentSeason(season) with the result. The master ignores its own broadcast (the same master() check short-circuits the handler), since it already set its cache directly inside createNewSeason()'s caller.

Player quest progress and points are not part of this packet — they travel independently through AstralSync's own cross-server replication of FarmPassData. See Per-player season sync.

Reroll

/pass reroll (permission farmpass.reroll) calls SeasonRotationService.rerollSeason():

  1. Looks up the current season. If there isn't one, it just calls createNewSeason().

  2. Otherwise, deletes every QuestEntity belonging to that season (QuestRepository.deleteBySeasonId).

  3. Deletes the SeasonEntity row itself (SeasonRepository.delete).

  4. Calls QuestGenerationService.resetSeasonTracking(oldSeasonId) to drop the no-duplicate blueprint tracking for the deleted season.

  5. Calls createNewSeason() — a fresh SeasonEntity for the current month, fully repopulated with new daily and weekly quests exactly as automatic rotation would produce.

On success, seasonService.setCurrentSeason(newSeason) updates the local cache and the FarmpassUpdatedPacket broadcast (fired inside createNewSeason()) propagates the new season to every other node. Reroll is not restricted to the master node at the permission layer, but since it goes through the same SeasonRotationService instance, it only actually deletes/creates seasons on the node it's run on — running it on a non-master node would create a second, competing season row rather than being rejected.

Per-player season sync

Player quest progress, points, and claimed rewards live in FarmPassData, synced across the network via AstralSync's FarmPassSnapshotAdapter (SnapshotAdapter<FarmPassData>, key farmpass:data).

  • create(player) — for a player with no existing snapshot, builds a fresh FarmPassData with an empty claimed-rewards set, 0 points, the current season id (plugin.seasons().getCurrentSeasonId()), and that season's quests loaded via loadSeasonQuestsForPlayer.

  • update(player, farmPassData) — run against an existing snapshot. Reads the current season id; if there is none, logs a warning and returns the data unchanged. Otherwise, if the stored farmPassData.seasonId() is null or doesn't match the current season id, it:

    1. Calls farmPassData.reset() — clears the quest list, clears claimed rewards, and zeroes points (this does not clear seasonId — see below).

    2. Sets farmPassData.seasonId(currentSeasonId).

    3. Clears and repopulates the quest list from loadSeasonQuestsForPlayer(currentSeasonId).

loadSeasonQuestsForPlayer(seasonId) reads QuestService.findCachedQuests(seasonId) — the in-memory Caffeine cache populated by findBySeason (itself invoked from SeasonService.setCurrentSeason) — resolves each QuestEntity's blueprint, builds a QuestInstance with progress = 0, and sorts the result by unlocksAt. A quest whose blueprint can no longer be resolved (deleted/renamed) is dropped with a warning rather than failing the whole load.

Because this check runs on every sync update, a player's data is transparently reset the first time they're seen after a season rotation or reroll — there is no separate "season changed" event or migration step.

Further reading

  • Overview — how seasons, categories, quests, and rewards fit together.

  • Configuration — the master flag.

  • Commands/pass reroll.

  • Quests & Goals — blueprint format, QuestGoal types, and point crediting on completion.

Last modified: 25 July 2026