Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/main/java/com/yourorg/servershop/weekly/CronSchedule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.yourorg.servershop.weekly;

import java.time.*;
import java.util.*;

/**
* Minimal cron expression parser supporting five fields: minute, hour, day of
* month, month and day of week. Only simple numeric values, ranges and
* comma-separated lists are supported. This is sufficient for weekly schedules
* such as "0 0 * * MON".
*/
public final class CronSchedule {
private final Set<Integer> minutes;
private final Set<Integer> hours;
private final Set<Integer> doms;
private final Set<Integer> months;
private final Set<Integer> dows; // 1=Mon .. 7=Sun

public CronSchedule(String expr) {
String[] parts = expr.trim().split("\\s+");
if (parts.length != 5) throw new IllegalArgumentException("Cron must have 5 parts");
minutes = parsePart(parts[0], 0, 59);
hours = parsePart(parts[1], 0, 23);
doms = parsePart(parts[2], 1, 31);
months = parsePart(parts[3], 1, 12);
dows = parseDow(parts[4]);
}

private Set<Integer> parsePart(String part, int min, int max) {
Set<Integer> set = new LinkedHashSet<>();
if ("*".equals(part)) {
for (int i = min; i <= max; i++) set.add(i);
return set;
}
for (String t : part.split(",")) {
if (t.contains("-")) {
String[] lr = t.split("-");
int l = Integer.parseInt(lr[0]);
int r = Integer.parseInt(lr[1]);
for (int i = l; i <= r; i++) set.add(i);
} else {
set.add(Integer.parseInt(t));
}
}
return set;
}

private Set<Integer> parseDow(String part) {
Set<Integer> set = new LinkedHashSet<>();
if ("*".equals(part)) {
for (int i = 1; i <= 7; i++) set.add(i);
return set;
}
for (String t : part.split(",")) {
if (t.contains("-")) {
String[] lr = t.split("-");
int l = toDow(lr[0]);
int r = toDow(lr[1]);
for (int i = l; i <= r; i++) set.add(i);
} else {
set.add(toDow(t));
}
}
return set;
}

private int toDow(String s) {
try {
int v = Integer.parseInt(s);
return v == 0 ? 7 : v; // cron 0 or 7 = Sunday
} catch (NumberFormatException ignored) {
return DayOfWeek.valueOf(s.toUpperCase()).getValue();
}
}

private boolean matches(ZonedDateTime z) {
return minutes.contains(z.getMinute()) &&
hours.contains(z.getHour()) &&
doms.contains(z.getDayOfMonth()) &&
months.contains(z.getMonthValue()) &&
dows.contains(z.getDayOfWeek().getValue());
}

/**
* Returns the next instant strictly after {@code from} that matches the
* cron expression.
*/
public Instant next(Instant from, ZoneId zone) {
ZonedDateTime z = ZonedDateTime.ofInstant(from, zone).withSecond(0).withNano(0).plusMinutes(1);
for (int i = 0; i < 500000; i++) { // safety bound ~1 year
if (matches(z)) return z.toInstant();
z = z.plusMinutes(1);
}
return z.toInstant();
}
}

129 changes: 112 additions & 17 deletions src/main/java/com/yourorg/servershop/weekly/WeeklyShopManager.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,124 @@
package com.yourorg.servershop.weekly;

import com.yourorg.servershop.ServerShopPlugin;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;

import java.time.*;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.*;

/**
* Manages the list of weekly picks. Picks are rotated automatically according to
* a cron expression and chosen from a configurable pool (optionally weighted).
*/
public final class WeeklyShopManager {
private final ServerShopPlugin plugin;
private final int count;
private final Map<Material, Double> pool = new EnumMap<>(Material.class);
private final Set<Material> current = new LinkedHashSet<>();
private final Random random = new Random();
private final File file;
private final CronSchedule schedule;
private final ZoneId zone = ZoneId.of("UTC");
private long lastRotation = 0L;

public WeeklyShopManager(ServerShopPlugin plugin) { this.plugin = plugin; }

public boolean isWeekly(Material m) { return currentPicks().contains(m); }

public java.util.Set<Material> currentPicks() {
int count = Math.max(1, plugin.getConfig().getInt("weekly.count", 6));
LocalDate now = LocalDate.now();
DayOfWeek dow = DayOfWeek.valueOf(plugin.getConfig().getString("weekly.firstDayOfWeek", "MONDAY"));
LocalDate weekStart = now.with(java.time.temporal.TemporalAdjusters.previousOrSame(dow));
long seed = weekStart.toEpochDay();
java.util.List<Material> pool = new java.util.ArrayList<>(plugin.catalog().allMaterials());
pool.sort(java.util.Comparator.comparing(Enum::name));
java.util.Random rnd = new java.util.Random(seed);
java.util.Set<Material> pick = new java.util.LinkedHashSet<>();
while (pick.size() < Math.min(count, pool.size())) pick.add(pool.get(rnd.nextInt(pool.size())));
return pick;
public WeeklyShopManager(ServerShopPlugin plugin) {
this.plugin = plugin;
var cfg = plugin.getConfig().getConfigurationSection("weekly");
this.count = Math.max(1, cfg.getInt("count", 6));
this.schedule = new CronSchedule(cfg.getString("cron", "0 0 * * MON"));
loadPool(cfg);
this.file = new File(plugin.getDataFolder(), "weekly.yml");
load();
Instant now = Instant.now();
if (current.isEmpty() || lastRotation == 0L ||
schedule.next(Instant.ofEpochMilli(lastRotation), zone).isBefore(now)) {
rotate();
}
scheduleNext();
}

/**
* Returns true if the given material is in the current weekly picks.
*/
public synchronized boolean isWeekly(Material m) { return current.contains(m); }

/**
* Returns the set of current weekly picks.
*/
public synchronized Set<Material> currentPicks() { return new LinkedHashSet<>(current); }

private void loadPool(ConfigurationSection cfg) {
if (cfg.isConfigurationSection("pool")) {
ConfigurationSection sec = cfg.getConfigurationSection("pool");
for (String k : sec.getKeys(false)) {
Material m = Material.matchMaterial(k);
if (m != null) pool.put(m, Math.max(0.0, sec.getDouble(k, 1.0)));
}
} else if (cfg.isList("pool")) {
for (String s : cfg.getStringList("pool")) {
Material m = Material.matchMaterial(s);
if (m != null) pool.put(m, 1.0);
}
}
if (pool.isEmpty()) {
for (Material m : plugin.catalog().allMaterials()) pool.put(m, 1.0);
}
}

private synchronized void rotate() {
current.clear();
List<Map.Entry<Material, Double>> entries = new ArrayList<>(pool.entrySet());
for (int i = 0; i < Math.min(count, entries.size()); i++) {
double total = 0.0;
for (var e : entries) total += Math.max(0.0, e.getValue());
double r = random.nextDouble() * total;
double acc = 0.0;
Iterator<Map.Entry<Material, Double>> it = entries.iterator();
while (it.hasNext()) {
var e = it.next();
acc += Math.max(0.0, e.getValue());
if (r <= acc) { current.add(e.getKey()); it.remove(); break; }
}
}
lastRotation = System.currentTimeMillis();
save();
}

private synchronized void load() {
current.clear();
if (!file.exists()) return;
YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
lastRotation = y.getLong("lastRotation", 0L);
for (String s : y.getStringList("picks")) {
Material m = Material.matchMaterial(s);
if (m != null) current.add(m);
}
}

private synchronized void save() {
YamlConfiguration y = new YamlConfiguration();
y.set("lastRotation", lastRotation);
List<String> list = new ArrayList<>();
for (Material m : current) list.add(m.name());
y.set("picks", list);
try { y.save(file); } catch (IOException e) {
plugin.getLogger().warning("Failed to save weekly picks: " + e.getMessage());
}
}

private void scheduleNext() {
Instant next = schedule.next(Instant.ofEpochMilli(lastRotation), zone);
long delayTicks = Math.max(1L, (next.toEpochMilli() - System.currentTimeMillis()) / 50L);
Bukkit.getScheduler().runTaskLater(plugin, () -> {
rotate();
scheduleNext();
}, delayTicks);
}
}

2 changes: 2 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ weekly:
count: 6
discount: 0.80
firstDayOfWeek: MONDAY
cron: "0 0 * * MON" # Monday 00:00 UTC
pool: {}
priceModel:
minFactor: 0.5
maxFactor: 1.5
Expand Down
Loading