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 |
|---|---|---|
| String | The category id (should match its map key). |
|
| The nominal cadence of the category. |
| int | Points credited to a player's season points each time they complete a quest of this category. |
|
| The nominal regeneration cadence. |
| int | How many new quest instances to generate per regeneration period. |
| boolean | Whether previously generated quests of this category are meant to expire when a new period starts. |
Shipped categories
Category |
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Season lifecycle
A season is a SeasonEntity — uniqueId, 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 ( | Non-master node |
|---|---|---|
No current season | Calls | Logs "waiting for master to create one" and clears its cache ( |
Current season's | Calls | Logs "waiting for master to rotate" and clears its cache ( |
Current season still valid | No action. | No action. |
createNewSeason():
Builds a
SeasonEntityfor the current month (SeasonTimeUtils.getMonthStart/getMonthEnd).Saves it via
SeasonRepository.save.Calls
generateSeasonQueststo populate it with daily and weekly quests.Broadcasts a
FarmpassUpdatedPacket(channelfarmpass, packet id0x00) overMessagingServiceso 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):
Collects every
QuestBlueprintwhoserequirementsmap contains the givencategoryId— i.e. every blueprint that declares amin/maxrange for that category. Fails the future if none exist.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.Picks uniformly at random (
java.util.Random) from the eligible pool, then rolls a random target inside that blueprint'smin–maxrange for the category (random.nextInt(max - min + 1) + min).Saves a new
QuestEntity(id,seasonId,blueprintId, the rolled value,unlocksAt,expiresAt,createdAt) to thequeststable.
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.yml → master (see Configuration); exactly one node in the network should run with master: true.
Master: its
SeasonRotationServiceis the only one that callscreateNewSeason()— both from the periodic rotation check and from/pass reroll. After creating or rerolling a season it broadcastsFarmpassUpdatedPacket.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 thefarmpasschannel that — only whenplugin.configuration().master()isfalseand the packet is aFarmpassUpdatedPacket— re-readsfindCurrentSeason()from the database and callssetCurrentSeason(season)with the result. The master ignores its own broadcast (the samemaster()check short-circuits the handler), since it already set its cache directly insidecreateNewSeason()'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():
Looks up the current season. If there isn't one, it just calls
createNewSeason().Otherwise, deletes every
QuestEntitybelonging to that season (QuestRepository.deleteBySeasonId).Deletes the
SeasonEntityrow itself (SeasonRepository.delete).Calls
QuestGenerationService.resetSeasonTracking(oldSeasonId)to drop the no-duplicate blueprint tracking for the deleted season.Calls
createNewSeason()— a freshSeasonEntityfor 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 freshFarmPassDatawith an empty claimed-rewards set,0points, the current season id (plugin.seasons().getCurrentSeasonId()), and that season's quests loaded vialoadSeasonQuestsForPlayer.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 storedfarmPassData.seasonId()isnullor doesn't match the current season id, it:Calls
farmPassData.reset()— clears the quest list, clears claimed rewards, and zeroes points (this does not clearseasonId— see below).Sets
farmPassData.seasonId(currentSeasonId).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
masterflag.Commands —
/pass reroll.Quests & Goals — blueprint format,
QuestGoaltypes, and point crediting on completion.