Content generated with AI — it may contain mistakes.

Gameplay

Schedules

Timetables: what starts by itself, when, and what has to be true for it to happen.

PluginSchedules schedules = Schedules.of(this);
 
// The gate only this plugin can answer.
schedules.condition("event-inactive", s -> !events.isActive(s.target()));
// What a fire does.
schedules.onFire(s -> events.start(s.target()));
 
// Whenever the configuration is (re)loaded:
schedules.set(configs.stream().flatMap(c -> c.schedules().stream()).toList());

What it replaces

Every plugin that started something on a clock wrote the same scheduler: a repeating task, a list of entries parsed out of YAML, a comparison of the current hour and minute against each one, and a map of the last minute each entry fired in so the second tick of the same minute would not fire it twice. There were at least three copies, and they had already drifted into disagreeing about the key for the minimum player count.

The cost now is one asynchronous task for the whole server. Each pass reads one long per plugin and compares it to the clock; nothing is walked, parsed or compared until a schedule is due. The moment one is, the work moves onto the owning plugin's own scheduler before a single gate is evaluated, so a condition may touch the server and so may the handler.

A schedule

An immutable record, the same shape as every other configured thing in the library.

FieldWhat it means
nameWhat an admin calls the line. Blank shows the times instead.
targetWhat it starts. The meaning is the owning plugin's — normally a configuration id.
enabledWhether it may fire at all.
daysWhich days. Empty means every day.
timesThe clock times it fires at.
everyRepeat this often instead of using fixed times.
from / toThe window the repeat runs inside.
minPlayers / maxPlayersHow many players must be, and may be, online.
conditionAn Exylia comparison, such as %server_tps% >= 18.
requiresNamed gates the owning plugin registered.
cooldownThe shortest gap between two fires of this line.
Schedule.at("koth_desert", LocalTime.of(20, 0), LocalTime.of(22, 30))
        .withDays(Set.of(DayOfWeek.FRIDAY, DayOfWeek.SATURDAY));
 
Schedule.every("koth_desert", Duration.ofHours(2));   // then set from/to

A repeat is anchored to the window's own opening, not to the moment the plugin loaded: an every-two-hours schedule that opens at 10:00 fires at 10:00, 12:00 and 14:00 whether the server restarted at 13:07 or not.

There is no cron string

A cron string is unreadable in a menu and unwritable in a form, and every schedule any Exylia plugin has needed is "these times, these days, if these things hold".

Named gates

The thing configuration cannot express:

schedules.condition("event-inactive", s -> !events.isActive(s.target()));

A schedule listing event-inactive in its requires fires only when the test passes. The names a plugin offers are shown to the admin in the edit form, so nobody has to guess them.

An unknown gate fails closed

A schedule requiring a name the plugin never registered does not fire, and says so loudly. That is the deliberate opposite of rewards and effects, which fail open: withholding a reward is invisible, while starting an event the admin asked not to start is loud.

A fire stopped by a gate is skipped rather than retried — an event blocked at eight o'clock starting at eight minutes past is not what the timetable said. A fire more than two minutes late, after a frozen server or a suspended host, is skipped too: firing a day of missed schedules all at once on the way back up is worse than missing them.

Storing and reading them

String stored = ScheduleCodec.encode(schedules);
List<Schedule> schedules = ScheduleCodec.decode(stored);

A JSON array, so a timetable lives in a TEXT column beside the rewards and commands of the same row. Only what carries meaning is written, and an empty list stores as null rather than [].

ScheduleCodec.fromMapList reads the block form every plugin had before this module — both spellings of the player key included — so migrating is a read rather than an admin retyping their timetable:

- name: 'Friday night'
  target: 'koth_desert'
  time: '20:00'              # or times: ['20:00', '22:30']
  days: [FRIDAY, SATURDAY]   # or ['*'] for every day
  min-players: 10            # also read as min-players-online

Editing and reading the clock

schedules.editor(config.schedules(), config.id())
         .title("{primary}&lSCHEDULES")
         .onSave(edited -> configs.save(config.withSchedules(edited)))
         .open(player);

The list editor over schedules: pagination, add, edit, delete, copy and paste. Passing the target means the screen already knows what it starts, so the admin is not asked for an id they can only get wrong; passing null makes it a field.

schedules.nextFireOf("koth_desert");        // Optional<Instant>
schedules.millisUntilNext("koth_desert");   // OptionalLong, for a placeholder
schedules.isScheduled("koth_desert");
schedules.fireNow(schedule);                // early, but every gate is still checked

Timezone

One setting, in the library's own config.yml:

timezone: 'Europe/Madrid'

Empty means the host's own zone, which is right until the host and the players are in different countries. A plugin that genuinely runs on a different clock calls schedules.zone(...); nothing else should.

Something missing on this page? Tell us on Discord