diff --git a/core/src/main/java/to/itsme/itsmyconfig/ItsMyConfig.java b/core/src/main/java/to/itsme/itsmyconfig/ItsMyConfig.java index 7456e78..cc701f7 100644 --- a/core/src/main/java/to/itsme/itsmyconfig/ItsMyConfig.java +++ b/core/src/main/java/to/itsme/itsmyconfig/ItsMyConfig.java @@ -411,6 +411,8 @@ private Placeholder getPlaceholder(String filePath, final ConfigurationSection s case MATH -> new MathPlaceholder(filePath, section); case RANDOM -> new RandomPlaceholder(filePath, section); case LIST -> new ListPlaceholder(filePath, section); + case MAP -> new MapPlaceholder(filePath, section); + case RANGE -> new RangePlaceholder(filePath, section); case ANIMATION -> new AnimatedPlaceholder(filePath, section); case COLOR -> new ColorPlaceholder(filePath, section); case COLORED_TEXT -> new ColoredTextPlaceholder(filePath, section); diff --git a/core/src/main/java/to/itsme/itsmyconfig/placeholder/PlaceholderType.java b/core/src/main/java/to/itsme/itsmyconfig/placeholder/PlaceholderType.java index 0d039c6..d537a6b 100644 --- a/core/src/main/java/to/itsme/itsmyconfig/placeholder/PlaceholderType.java +++ b/core/src/main/java/to/itsme/itsmyconfig/placeholder/PlaceholderType.java @@ -20,6 +20,11 @@ public enum PlaceholderType { * Represents a placeholder type for getting a value of a list using the (index + 1). */ LIST, + /** + * Represents a placeholder type for getting a value from a map using a key. + */ + MAP, + RANGE, /** * Represents a placeholder type for getting random values out of a list. */ diff --git a/core/src/main/java/to/itsme/itsmyconfig/placeholder/type/MapPlaceholder.java b/core/src/main/java/to/itsme/itsmyconfig/placeholder/type/MapPlaceholder.java new file mode 100644 index 0000000..cf2d63b --- /dev/null +++ b/core/src/main/java/to/itsme/itsmyconfig/placeholder/type/MapPlaceholder.java @@ -0,0 +1,101 @@ +package to.itsme.itsmyconfig.placeholder.type; + +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.ConfigurationSection; +import to.itsme.itsmyconfig.placeholder.Placeholder; +import to.itsme.itsmyconfig.placeholder.PlaceholderDependancy; +import to.itsme.itsmyconfig.placeholder.PlaceholderType; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * YAML: + * custom-placeholder: + * map-type-placeholder: + * type: map + * # Optional: + * # default: "" + * # ignorecase: true + * values: + * Key1: "some value" + * keY2: "other value" + * + * Usage: + * %itsmyconfig_map-type-placeholder_Key1% + * or with args: + * %itsmyconfig_map-type-placeholder_Key1::% + */ +public final class MapPlaceholder extends Placeholder { + + private final Map map; + private final boolean ignoreCase; + private final String defaultValue; + + public MapPlaceholder( + final String filePath, + final ConfigurationSection section + ) { + super(section, filePath, PlaceholderType.MAP, PlaceholderDependancy.NONE); + + this.ignoreCase = section.getBoolean("ignorecase", false); + this.defaultValue = section.getString("default", ""); + + final ConfigurationSection values = section.getConfigurationSection("values"); + if (values == null) { + this.map = Collections.emptyMap(); + return; + } + + // Pre-size for fewer rehashes (HashMap capacity ~ size/0.75 + 1) + final int size = values.getKeys(false).size(); + final int capacity = (int) (size / 0.75f) + 1; + + final Map tmp = new HashMap<>(Math.max(16, capacity)); + for (final String key : values.getKeys(false)) { + final Object raw = values.get(key); + final String value = raw == null ? "" : String.valueOf(raw); + tmp.put(normalizeKey(key), value); + } + + this.map = Collections.unmodifiableMap(tmp); + } + + @Override + public String getResult( + final OfflinePlayer player, + final String[] args + ) { + if (args.length == 0) { + return defaultValue; + } + + final String key = normalizeKey(args[0]); + final String value = map.get(key); + if (value == null || value.isEmpty()) { + return defaultValue; + } + + // Optional: support {0},{1}... substitution from remaining args + if (args.length == 1) { + return value; + } + + return applyArgs(value, args); + } + + private String normalizeKey(final String key) { + if (key == null) return ""; + return ignoreCase ? key.toLowerCase(java.util.Locale.ROOT) : key; + } + + private static String applyArgs(final String template, final String[] args) { + String out = template; + // args[0] is the map-key; replacements start from args[1] -> {0} + for (int i = 1; i < args.length; i++) { + out = out.replace("{" + (i - 1) + "}", args[i]); + } + return out; + } +} diff --git a/core/src/main/java/to/itsme/itsmyconfig/placeholder/type/RangePlaceholder.java b/core/src/main/java/to/itsme/itsmyconfig/placeholder/type/RangePlaceholder.java new file mode 100644 index 0000000..fd478a3 --- /dev/null +++ b/core/src/main/java/to/itsme/itsmyconfig/placeholder/type/RangePlaceholder.java @@ -0,0 +1,253 @@ +package to.itsme.itsmyconfig.placeholder.type; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.ConfigurationSection; +import to.itsme.itsmyconfig.placeholder.Placeholder; +import to.itsme.itsmyconfig.placeholder.PlaceholderDependancy; +import to.itsme.itsmyconfig.placeholder.PlaceholderType; + +import java.util.*; +import java.util.logging.Logger; + +/** + * YAML: + * custom-placeholder: + * range-example: + * type: range + * default: "" + * values: + * "1-90": "someCommand {0} %some_placeholder%" + * "91-120": "otherCommand {0} %another_placeholder%" + * + * Supported keys (inclusive): + * - "A-B" (A..B) + * - "-B" (MIN..B) + * - "A-" (A..MAX) + * + * Usage: + * %itsmyconfig_range-example_::::...% + * + * Notes: + * - args[0] = (used for interval selection) + * - args[1] -> {0}, args[2] -> {1}, etc. + * + * Example: + * %itsmyconfig_range-example_95::PlayerName% + * args[0]=95, args[1]=PlayerName -> returns: "otherCommand PlayerName %another_placeholder%" + */ + +public final class RangePlaceholder extends Placeholder { + + private static final Logger LOGGER = Bukkit.getLogger(); + + private final String defaultValue; + private final long[] starts; + private final long[] ends; + private final String[] values; + + public RangePlaceholder( + final String filePath, + final ConfigurationSection section + ) { + super(section, filePath, PlaceholderType.RANGE, PlaceholderDependancy.NONE); + + this.defaultValue = section.getString("default", ""); + + final ConfigurationSection cfg = section.getConfigurationSection("values"); + if (cfg == null) { + this.starts = new long[0]; + this.ends = new long[0]; + this.values = new String[0]; + return; + } + + // Parse all entries + final List entries = new ArrayList<>(); + for (final String key : cfg.getKeys(false)) { + final Object raw = cfg.get(key); + final String value = raw == null ? "" : String.valueOf(raw); + + final Range r = parseRangeKey(key); + if (r == null) { + warn(section, "Invalid range key '" + key + "' (skipped). Expected A-B, -B, or A-"); + continue; + } + if (r.start > r.end) { + warn(section, "Range key '" + key + "' has start > end (skipped)."); + continue; + } + + entries.add(new Entry(r.start, r.end, value, key)); + } + + // Sort by start, then end + entries.sort(Comparator + .comparingLong((Entry e) -> e.start) + .thenComparingLong(e -> e.end)); + + // Detect overlaps: "first wins", later overlapping entries are skipped with warning + final List filtered = new ArrayList<>(entries.size()); + Entry prev = null; + for (final Entry cur : entries) { + if (prev != null && cur.start <= prev.end) { + warn(section, + "Overlapping ranges: '" + prev.originalKey + "' [" + prev.start + "-" + prev.end + "] " + + "overlaps with '" + cur.originalKey + "' [" + cur.start + "-" + cur.end + "]. " + + "Keeping the first, skipping '" + cur.originalKey + "'."); + continue; + } + filtered.add(cur); + prev = cur; + } + + // Store in arrays for fast lookup + final int n = filtered.size(); + final long[] s = new long[n]; + final long[] e = new long[n]; + final String[] v = new String[n]; + + for (int i = 0; i < n; i++) { + final Entry it = filtered.get(i); + s[i] = it.start; + e[i] = it.end; + v[i] = it.value; + } + + this.starts = s; + this.ends = e; + this.values = v; + } + + @Override + public String getResult( + final OfflinePlayer player, + final String[] args + ) { + if (args.length == 0) { + return defaultValue; + } + + final long x; + try { + x = Long.parseLong(args[0]); + } catch (final Exception ignored) { + return defaultValue; + } + + final int idx = findRangeIndex(x); + if (idx < 0) { + return defaultValue; + } + + final String template = values[idx]; + if (template == null || template.isEmpty()) { + return defaultValue; + } + + if (args.length == 1) { + return template; + } + + return applyArgs(template, args); + } + + /** + * Binary search by start, then check containment. + */ + private int findRangeIndex(final long x) { + if (starts.length == 0) return -1; + + int lo = 0, hi = starts.length - 1; + int best = -1; + + // find rightmost start <= x + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + if (starts[mid] <= x) { + best = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + + if (best == -1) return -1; + return (x <= ends[best]) ? best : -1; + } + + private static Range parseRangeKey(final String keyRaw) { + if (keyRaw == null) return null; + final String key = keyRaw.trim(); + if (key.isEmpty()) return null; + + // "-B" + if (key.startsWith("-") && key.length() > 1) { + final long end = parseLongSafe(key.substring(1)); + if (end == Long.MIN_VALUE) return null; + return new Range(Long.MIN_VALUE, end); + } + + // "A-" + if (key.endsWith("-") && key.length() > 1) { + final long start = parseLongSafe(key.substring(0, key.length() - 1)); + if (start == Long.MIN_VALUE) return null; + return new Range(start, Long.MAX_VALUE); + } + + // "A-B" + final int dash = key.indexOf('-'); + if (dash <= 0 || dash >= key.length() - 1) return null; + + final long start = parseLongSafe(key.substring(0, dash)); + final long end = parseLongSafe(key.substring(dash + 1)); + if (start == Long.MIN_VALUE || end == Long.MIN_VALUE) return null; + + return new Range(start, end); + } + + private static long parseLongSafe(final String s) { + try { + return Long.parseLong(s.trim()); + } catch (final Exception ignored) { + return Long.MIN_VALUE; + } + } + + private static String applyArgs(final String template, final String[] args) { + String out = template; + // args[0] is the number; replacements start from args[1] -> {0} + for (int i = 1; i < args.length; i++) { + out = out.replace("{" + (i - 1) + "}", args[i]); + } + return out; + } + + private static void warn(final ConfigurationSection section, final String msg) { + LOGGER.warning("[ItsMyConfig] Range placeholder misconfig at '" + section.getCurrentPath() + "': " + msg); + } + + private static final class Range { + final long start; + final long end; + + Range(final long start, final long end) { + this.start = start; + this.end = end; + } + } + + private static final class Entry { + final long start; + final long end; + final String value; + final String originalKey; + + Entry(final long start, final long end, final String value, final String originalKey) { + this.start = start; + this.end = end; + this.value = value; + this.originalKey = originalKey; + } + } +}