diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
index 8ff3e68..c3cabca 100644
--- a/config/checkstyle/suppressions.xml
+++ b/config/checkstyle/suppressions.xml
@@ -35,6 +35,13 @@
+
+
+
diff --git a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
index b040b20..a82ad3b 100644
--- a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
+++ b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
@@ -47,9 +47,9 @@ public final class PluginBootstrap {
public static void enable(@NotNull LoParkour plugin) {
Config.reload(true);
- AdaptiveServices.init(plugin);
loadSchematics(plugin);
registerModes();
+ AdaptiveServices.init(plugin);
registerHooks(plugin);
setupWorld(plugin);
registerEventsAndCommands(plugin);
diff --git a/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeParser.java b/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeParser.java
index 06e05ab..21cd2a9 100644
--- a/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeParser.java
+++ b/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeParser.java
@@ -10,6 +10,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
@@ -36,26 +37,105 @@ public final class ConfigMergeParser {
@NotNull
public Map extractFullPathValues(@NotNull List lines) {
final Map values = new LinkedHashMap<>();
- final Deque stack = new ArrayDeque<>();
- int prevIndent = -1;
+ final Deque stack = new ArrayDeque<>();
for (final String line : lines) {
final LineTokens tokens = parseLine(line);
if (tokens == null) {
continue;
}
- prevIndent = popToLevel(stack, tokens.indent(), prevIndent);
+ popToLevel(stack, tokens.indent());
final String fullPath = resolvePath(stack, tokens.leaf());
if (tokens.isSection()) {
- stack.push(tokens.leaf());
+ stack.push(new Section(tokens.leaf(), tokens.indent()));
} else {
values.put(fullPath, tokens.after());
}
- prevIndent = tokens.indent();
}
return values;
}
+ @NotNull
+ public Map> extractBlockSequences(@NotNull List lines) {
+ final Map> lists = new LinkedHashMap<>();
+ final Deque stack = new ArrayDeque<>();
+
+ for (int i = 0; i < lines.size(); i++) {
+ final LineTokens tokens = parseLine(lines.get(i));
+ if (tokens == null) {
+ continue;
+ }
+ popToLevel(stack, tokens.indent());
+ final String fullPath = resolvePath(stack, tokens.leaf());
+ if (tokens.isSection()) {
+ final List items = tokens.after().isEmpty() ? listItems(lines, i + 1, tokens.indent()) : null;
+ if (items != null) {
+ lists.put(fullPath, items);
+ }
+ stack.push(new Section(tokens.leaf(), tokens.indent()));
+ }
+ }
+ return lists;
+ }
+
+ @Nullable
+ private static List listItems(@NotNull List lines, int from, int sectionIndent) {
+ final int start = firstListItemIndex(lines, from, sectionIndent);
+ if (start < 0) {
+ return null;
+ }
+ final List items = new ArrayList<>();
+ for (int j = start; j < listEnd(lines, start); j++) {
+ items.add(lines.get(j).trim());
+ }
+ return items;
+ }
+
+ @NotNull
+ public static List pathsFor(@NotNull List lines) {
+ final List paths = new ArrayList<>(lines.size());
+ final Deque stack = new ArrayDeque<>();
+
+ for (final String line : lines) {
+ final LineTokens tokens = parseLine(line);
+ if (tokens == null) {
+ paths.add(null);
+ continue;
+ }
+ popToLevel(stack, tokens.indent());
+ final String fullPath = resolvePath(stack, tokens.leaf());
+ if (tokens.isSection()) {
+ stack.push(new Section(tokens.leaf(), tokens.indent()));
+ }
+ paths.add(fullPath);
+ }
+ return paths;
+ }
+
+ static boolean isListItem(@NotNull String line) {
+ final String trimmed = line.trim();
+ return "-".equals(trimmed) || trimmed.startsWith("- ");
+ }
+
+ static int firstListItemIndex(@NotNull List lines, int from, int sectionIndent) {
+ for (int i = from; i < lines.size(); i++) {
+ final String trimmed = lines.get(i).trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+ continue;
+ }
+ return isListItem(lines.get(i)) && indentOf(lines.get(i)) > sectionIndent ? i : -1;
+ }
+ return -1;
+ }
+
+ static int listEnd(@NotNull List lines, int start) {
+ int j = start;
+ while (j < lines.size() && isListItem(lines.get(j))) {
+ j++;
+ }
+ return j;
+ }
+
@Nullable
public static LineTokens parseLine(@NotNull String line) {
if (isSkippable(line)) {
@@ -80,20 +160,18 @@ public final class ConfigMergeParser {
return after.isEmpty() || after.startsWith("#");
}
- public static int popToLevel(@NotNull Deque stack, int indent, int prevIndent) {
- int current = prevIndent;
- while (current >= 0 && indent <= current) {
- if (!stack.isEmpty()) {
- stack.pop();
- }
- current -= 2;
+ private static void popToLevel(@NotNull Deque stack, int indent) {
+ while (!stack.isEmpty() && stack.peek().indent() >= indent) {
+ stack.pop();
}
- return current;
}
@NotNull
- public static String resolvePath(@NotNull Deque stack, @NotNull String leaf) {
- return stack.isEmpty() ? leaf : String.join(".", stack) + "." + leaf;
+ private static String resolvePath(@NotNull Deque stack, @NotNull String leaf) {
+ if (stack.isEmpty()) {
+ return leaf;
+ }
+ return String.join(".", stack.stream().map(Section::name).toList().reversed()) + "." + leaf;
}
public static int indentOf(@NotNull String line) {
@@ -104,6 +182,9 @@ public final class ConfigMergeParser {
return i;
}
+ private record Section(String name, int indent) {
+ }
+
public record LineTokens(String leaf, String after, int indent, boolean isSection) {
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeProcessor.java b/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeProcessor.java
index 4494df8..385fc65 100644
--- a/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeProcessor.java
+++ b/src/main/java/dev/loki/loparkour/config/core/merge/ConfigMergeProcessor.java
@@ -6,18 +6,13 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.Deque;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-/**
- * Builds the merged file by walking the template and substituting any scalar
- * value that the user has already customized, preserving comments and sections.
- */
public final class ConfigMergeProcessor {
private final ConfigMergeParser parser = new ConfigMergeParser();
@@ -27,44 +22,106 @@ public final class ConfigMergeProcessor {
@NotNull InputStream templateStream,
@NotNull File existingFile,
@Nullable List ignoredKeys) throws IOException {
-
final List templateLines = parser.readLines(templateStream);
final List existingLines = existingFile.exists() ? parser.readLines(existingFile) : List.of();
+ return merge(templateLines, existingLines, ignoredKeys);
+ }
+ @NotNull
+ List merge(
+ @NotNull List templateLines,
+ @NotNull List existingLines,
+ @Nullable List ignoredKeys) {
final Set ignored = normalizeIgnored(ignoredKeys);
final Map existingValues = parser.extractFullPathValues(existingLines);
-
+ final Map> existingLists = parser.extractBlockSequences(existingLines);
+ final Map> existingSections = extractSections(existingLines);
+ final List templatePaths = ConfigMergeParser.pathsFor(templateLines);
final List result = new ArrayList<>();
- final Deque stack = new ArrayDeque<>();
- int prevIndent = -1;
-
- for (final String line : templateLines) {
+ int i = 0;
+ while (i < templateLines.size()) {
+ final String line = templateLines.get(i);
final ConfigMergeParser.LineTokens tokens = ConfigMergeParser.parseLine(line);
if (tokens == null) {
result.add(line);
+ i++;
continue;
}
- prevIndent = ConfigMergeParser.popToLevel(stack, tokens.indent(), prevIndent);
- final String fullPath = ConfigMergeParser.resolvePath(stack, tokens.leaf());
if (tokens.isSection()) {
- stack.push(tokens.leaf());
- result.add(line);
- } else if (isIgnored(fullPath, ignored)) {
- result.add(line);
+ i = appendSection(result, templateLines, i, templatePaths.get(i), ignored, existingSections, existingLists);
} else {
- result.add(applyExistingValue(line, fullPath, existingValues));
+ result.add(applyExistingValue(line, templatePaths.get(i), existingValues));
+ i++;
}
- prevIndent = tokens.indent();
}
return result;
}
+ private static int appendSection(
+ @NotNull List result,
+ @NotNull List templateLines,
+ int i,
+ @NotNull String fullPath,
+ @NotNull Set ignored,
+ @NotNull Map> existingSections,
+ @NotNull Map> existingLists) {
+ if (isIgnored(fullPath, ignored)) {
+ final int next = appendIgnoredSection(result, templateLines, i, fullPath, existingSections);
+ if (next > i) {
+ return next;
+ }
+ }
+ result.add(templateLines.get(i));
+ return appendBlockList(result, templateLines, i, fullPath, ConfigMergeParser.indentOf(templateLines.get(i)), existingLists);
+ }
+
+ private static int appendIgnoredSection(
+ @NotNull List result,
+ @NotNull List templateLines,
+ int i,
+ @NotNull String fullPath,
+ @NotNull Map> existingSections) {
+ final List userSection = existingSections.get(fullPath);
+ if (userSection == null) {
+ return i;
+ }
+ final int delta = ConfigMergeParser.indentOf(templateLines.get(i)) - ConfigMergeParser.indentOf(userSection.get(0));
+ for (final String sectionLine : userSection) {
+ result.add(reindent(sectionLine, delta));
+ }
+ return i + sectionSpan(templateLines, ConfigMergeParser.pathsFor(templateLines), i).size();
+ }
+
+ private static int appendBlockList(
+ @NotNull List result,
+ @NotNull List templateLines,
+ int i,
+ @NotNull String fullPath,
+ int sectionIndent,
+ @NotNull Map> existingLists) {
+ final int listStart = ConfigMergeParser.firstListItemIndex(templateLines, i + 1, sectionIndent);
+ if (listStart < 0) {
+ return i + 1;
+ }
+ final List userItems = existingLists.get(fullPath);
+ if (userItems == null) {
+ return i + 1;
+ }
+ final int listIndent = ConfigMergeParser.indentOf(templateLines.get(listStart));
+ for (int k = i + 1; k < listStart; k++) {
+ result.add(templateLines.get(k));
+ }
+ for (final String item : userItems) {
+ result.add(" ".repeat(listIndent) + item);
+ }
+ return ConfigMergeParser.listEnd(templateLines, listStart);
+ }
+
@NotNull
private static String applyExistingValue(
@NotNull String line,
@NotNull String fullPath,
@NotNull Map existingValues) {
-
final String existing = existingValues.get(fullPath);
if (existing == null) {
return line;
@@ -72,6 +129,52 @@ public final class ConfigMergeProcessor {
return line.substring(0, line.indexOf(':') + 1) + " " + existing;
}
+ @NotNull
+ private static String reindent(@NotNull String line, int delta) {
+ if (line.isEmpty()) {
+ return line;
+ }
+ final int currentIndent = ConfigMergeParser.indentOf(line);
+ return " ".repeat(Math.max(0, currentIndent + delta)) + line.substring(currentIndent);
+ }
+
+ @NotNull
+ private static Map> extractSections(@NotNull List lines) {
+ final Map> sections = new LinkedHashMap<>();
+ final List paths = ConfigMergeParser.pathsFor(lines);
+ for (int i = 0; i < lines.size(); i++) {
+ final ConfigMergeParser.LineTokens tokens = ConfigMergeParser.parseLine(lines.get(i));
+ if (tokens != null && tokens.isSection()) {
+ sections.put(paths.get(i), sectionSpan(lines, paths, i));
+ }
+ }
+ return sections;
+ }
+
+ private static List sectionSpan(
+ @NotNull List lines,
+ @NotNull List paths,
+ int start) {
+ final String path = paths.get(start);
+ final int indent = ConfigMergeParser.indentOf(lines.get(start));
+ final List span = new ArrayList<>();
+ int j = start;
+ while (j < lines.size()) {
+ final String candidate = paths.get(j);
+ if (j > start && candidate != null && !candidate.startsWith(path + ".")) {
+ break;
+ }
+ span.add(lines.get(j));
+ j++;
+ }
+ while (span.size() > 1
+ && ConfigMergeParser.parseLine(span.get(span.size() - 1)) == null
+ && ConfigMergeParser.indentOf(span.get(span.size() - 1)) <= indent) {
+ span.remove(span.size() - 1);
+ }
+ return span;
+ }
+
@NotNull
private static Set normalizeIgnored(@Nullable List keys) {
if (keys == null) {
@@ -93,4 +196,4 @@ public final class ConfigMergeProcessor {
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java b/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java
index a9def71..6917ff6 100644
--- a/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java
+++ b/src/main/java/dev/loki/loparkour/generator/core/model/GeneratorState.java
@@ -38,6 +38,7 @@ public class GeneratorState {
// Iteration is safe without external synchronization as all mutations happen on the same thread.
public final List history = new ArrayList<>();
public int lastPositionIndexPlayer = -1;
+ public int generatedBlocks = 0;
// Schematic state
public boolean deleteSchematic = false;
diff --git a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java
index cd152ea..a0807a8 100644
--- a/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java
+++ b/src/main/java/dev/loki/loparkour/generator/jump/calculation/JumpCalculator.java
@@ -1,9 +1,10 @@
package dev.loki.loparkour.generator.jump.calculation;
-import dev.loki.loparkour.generator.core.model.GeneratorOption;
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
+import dev.loki.loparkour.generator.core.model.GeneratorOption;
import dev.loki.loparkour.generator.jump.placement.BlockSelector;
import dev.loki.loparkour.util.misc.MaterialUtil;
+import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
@@ -11,6 +12,10 @@ import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
/**
* Calculates jump positions and validates jump constraints.
*/
@@ -21,7 +26,13 @@ public class JumpCalculator {
private static final int DEFAULT_MAX_DISTANCE = 4;
private static final int RESTRICTED_MAX_DISTANCE = 3;
private static final double RANDOM_OFFSET_SD = 0.5;
+ private static final double DEFAULT_RANDOM_OFFSET_SD = 1.0;
private static final int MAX_RETRY_ATTEMPTS = 10;
+ private static final int MAX_REROLL_ATTEMPTS = 3;
+ private static final int SAFETY_STEP_DISTANCE = 1;
+ private static final int SAFETY_STEP_HEIGHT = 0;
+ private static final double NO_LATERAL_JITTER = 0.0;
+ private static final double ZERO_HEADING_EPSILON = 1.0E-6;
private static final int PACKED_ICE_MAX_DISTANCE = 4;
private static final int BLUE_ICE_MAX_DISTANCE = 4;
private static final int ICE_MAX_DISTANCE = 3;
@@ -29,22 +40,21 @@ public class JumpCalculator {
private final ParkourGenerator generator;
private final BlockSelector blockSelector;
-
+
public JumpCalculator(@NotNull ParkourGenerator generator, @NotNull BlockSelector blockSelector) {
this.generator = generator;
this.blockSelector = blockSelector;
}
-
+
/**
* Calculate the next block position based on current block and jump parameters.
*/
@NotNull
public Block calculateNextBlock(@NotNull Block current, int distance, int height) {
- // Ensure zone is initialized
if (generator.state.zone == null || generator.state.zone.length < 2) {
throw new IllegalStateException("Generator zone not initialized");
}
-
+
final JumpDirector director = new JumpDirector(
BoundingBox.of(generator.state.zone[0], generator.state.zone[1]),
current.getLocation().toVector()
@@ -53,12 +63,10 @@ public class JumpCalculator {
generator.state.heading = director.getRecommendedHeading(generator.state.heading);
int resolvedHeight = director.getRecommendedHeight(height);
- // Apply material-specific restrictions
final JumpConstraints constraints = calculateJumpConstraints(current);
resolvedHeight = Math.min(resolvedHeight, constraints.maxHeight);
int resolvedDistance = Math.min(distance, constraints.maxDistance);
-
- // Clamp values to valid ranges
+
resolvedHeight = Math.max(MIN_HEIGHT, Math.min(2, resolvedHeight));
resolvedDistance = Math.max(1, Math.min(MAX_VALID_DISTANCE, resolvedDistance));
@@ -66,33 +74,28 @@ public class JumpCalculator {
resolvedDistance = Math.max(resolvedDistance - resolvedHeight, 1);
}
- // Calculate offset
- final Vector offset = calculateJumpOffset(resolvedDistance, resolvedHeight);
- final Block candidate = current.getLocation().add(offset).getBlock();
-
- // Validate jump with retry mechanism
- return validateAndRetryJump(current, candidate, resolvedDistance, resolvedHeight);
+ final double randomOffsetSd = Arrays.asList(generator.generatorOptions)
+ .contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? RANDOM_OFFSET_SD : DEFAULT_RANDOM_OFFSET_SD;
+ final Location candidate = validateAndRetryJump(current.getLocation(), generator.state.heading,
+ resolvedDistance, resolvedHeight, ThreadLocalRandom.current(), randomOffsetSd);
+ return candidate.getBlock();
}
-
- /**
- * Calculate jump constraints based on the material of the current block.
- */
+
@NotNull
public JumpConstraints calculateJumpConstraints(@NotNull Block currentBlock) {
final Material material = currentBlock.getType();
final BlockData blockData = currentBlock.getBlockData();
-
- // Default constraints
+
int maxHeight = 1;
int maxDistance = DEFAULT_MAX_DISTANCE;
-
+
if (MaterialUtil.isSlabMaterial(material)) {
if (blockData instanceof org.bukkit.block.data.type.Slab slab) {
if (slab.getType() == org.bukkit.block.data.type.Slab.Type.BOTTOM) {
- maxHeight = -1; // Bottom slab: reduced height capability
+ maxHeight = -1;
maxDistance = RESTRICTED_MAX_DISTANCE;
} else {
- maxHeight = 0; // Top slab: normal restrictions
+ maxHeight = 0;
maxDistance = RESTRICTED_MAX_DISTANCE;
}
} else {
@@ -106,77 +109,89 @@ public class JumpCalculator {
maxHeight = getSpecialMaterialMaxHeight(material);
maxDistance = getSpecialMaterialMaxDistance(material);
}
-
+
return new JumpConstraints(maxHeight, maxDistance);
}
-
- @NotNull
- private Vector calculateJumpOffset(int distance, int height) {
- final double sd = java.util.Arrays.asList(generator.generatorOptions)
- .contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? RANDOM_OFFSET_SD : 1;
- final int randomOffset = Math.min(
- new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd),
- (int) Math.floor(Math.sqrt(JumpValidator.MAX_HORIZONTAL_DISTANCE * JumpValidator.MAX_HORIZONTAL_DISTANCE - distance * distance))
- );
- final Vector offset = generator.state.heading.clone().multiply(distance).setY(height);
- if (offset.getX() == 0) {
- offset.setX(randomOffset);
- } else {
- offset.setZ(randomOffset);
+ @NotNull
+ static Vector offsetFor(@NotNull Vector heading, int distance, int height, double randomOffset) {
+ final Vector horizontal = new Vector(heading.getX(), 0, heading.getZ());
+ final double length = horizontal.length();
+ if (length < ZERO_HEADING_EPSILON) {
+ return new Vector(0, height, 0);
}
-
- return offset;
+ final Vector perpendicular = new Vector(-horizontal.getZ() / length, 0, horizontal.getX() / length);
+ return horizontal.multiply(distance).add(perpendicular.multiply(randomOffset)).setY(height);
}
-
+
+ static int randomOffset(int height, int distance, double standardDeviation, @NotNull Random random) {
+ final int maxJitter = (int) Math.floor(
+ Math.sqrt(JumpValidator.MAX_HORIZONTAL_DISTANCE * JumpValidator.MAX_HORIZONTAL_DISTANCE - distance * distance));
+ return Math.min(new JumpOffsetGenerator(height, distance).getRandomOffset(0, standardDeviation, random), maxJitter);
+ }
+
@NotNull
- private Block validateAndRetryJump(@NotNull Block current, @NotNull Block candidate, int distance, int height) {
+ static Location validateAndRetryJump(@NotNull Location current, @NotNull Vector heading, int distance, int height,
+ @NotNull Random random, double randomOffsetSd) {
final JumpValidator validator = new JumpValidator();
int resolvedDistance = distance;
int resolvedHeight = height;
- Block resolvedCandidate = candidate;
int attempts = 0;
-
- while (!validator.canJump(current.getLocation(), resolvedCandidate.getLocation()) && attempts < MAX_RETRY_ATTEMPTS) {
- // Reduce distance and height to make jump easier
+ Location candidate = current.clone().add(offsetFor(heading, resolvedDistance, resolvedHeight,
+ randomOffset(resolvedHeight, resolvedDistance, randomOffsetSd, random)));
+ while (!validator.canJump(current, candidate) && attempts < MAX_RETRY_ATTEMPTS) {
resolvedDistance = Math.max(1, resolvedDistance - 1);
resolvedHeight = Math.max(MIN_HEIGHT, resolvedHeight - 1);
-
- final Vector offset = calculateJumpOffset(resolvedDistance, resolvedHeight);
- resolvedCandidate = current.getLocation().add(offset).getBlock();
+ candidate = current.clone().add(offsetFor(heading, resolvedDistance, resolvedHeight,
+ randomOffset(resolvedHeight, resolvedDistance, randomOffsetSd, random)));
attempts++;
}
-
- return resolvedCandidate;
+
+ if (validator.canJump(current, candidate)) {
+ return candidate;
+ }
+ return safetyFallback(current, heading, randomOffsetSd, random, validator);
}
-
+
+ @NotNull
+ static Location safetyFallback(@NotNull Location current, @NotNull Vector heading, double randomOffsetSd,
+ @NotNull Random random, @NotNull JumpValidator validator) {
+ for (int i = 0; i < MAX_REROLL_ATTEMPTS; i++) {
+ final Location reRolled = current.clone().add(offsetFor(heading, SAFETY_STEP_DISTANCE, SAFETY_STEP_HEIGHT,
+ randomOffset(SAFETY_STEP_HEIGHT, SAFETY_STEP_DISTANCE, randomOffsetSd, random)));
+ if (validator.canJump(current, reRolled)) {
+ return reRolled;
+ }
+ }
+ final Location straight = current.clone().add(
+ offsetFor(heading, SAFETY_STEP_DISTANCE, SAFETY_STEP_HEIGHT, NO_LATERAL_JITTER));
+ return validator.canJump(current, straight) ? straight : current;
+ }
+
private int getSpecialMaterialMaxHeight(@NotNull Material material) {
return switch (material) {
- case PACKED_ICE -> 1; // Ice allows normal height
- case BLUE_ICE -> 1; // Blue ice same
- case ICE -> 0; // Regular ice melts, slippery
- case LADDER -> 1; // Ladder allows some height
- default -> 0; // Most special materials restrict height
+ case PACKED_ICE -> 1;
+ case BLUE_ICE -> 1;
+ case ICE -> 0;
+ case LADDER -> 1;
+ default -> 0;
};
}
private int getSpecialMaterialMaxDistance(@NotNull Material material) {
return switch (material) {
- case PACKED_ICE -> PACKED_ICE_MAX_DISTANCE; // Ice — full distance, slippery
- case BLUE_ICE -> BLUE_ICE_MAX_DISTANCE; // Blue ice — full distance
- case ICE -> ICE_MAX_DISTANCE; // Regular ice — reduced
- case LADDER -> 2; // Ladder is very restrictive
- default -> DEFAULT_SPECIAL_MAX_DISTANCE; // Default restriction
+ case PACKED_ICE -> PACKED_ICE_MAX_DISTANCE;
+ case BLUE_ICE -> BLUE_ICE_MAX_DISTANCE;
+ case ICE -> ICE_MAX_DISTANCE;
+ case LADDER -> 2;
+ default -> DEFAULT_SPECIAL_MAX_DISTANCE;
};
}
- /**
- * Represents jump constraints for a specific block type.
- */
public static class JumpConstraints {
public final int maxHeight;
public final int maxDistance;
-
+
public JumpConstraints(int maxHeight, int maxDistance) {
this.maxHeight = maxHeight;
this.maxDistance = maxDistance;
diff --git a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java
index 1faf4f2..08e96d9 100644
--- a/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java
+++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/BlockPlacer.java
@@ -54,6 +54,7 @@ public class BlockPlacer {
worldPlacer.place(block, blockData);
generator.state.history.add(block);
+ generator.state.generatedBlocks++;
generator.state.playerSpawn = spawn;
}
@@ -100,6 +101,7 @@ public class BlockPlacer {
final Block selectedBlock = blocks.get(0);
worldPlacer.place(selectedBlock, blockData);
generator.state.history.add(selectedBlock);
+ generator.state.generatedBlocks++;
}
private void placeSpecialBlock() {
@@ -118,5 +120,6 @@ public class BlockPlacer {
final Block selectedBlock = blocks.get(0);
worldPlacer.place(selectedBlock, specialBlockData);
generator.state.history.add(selectedBlock);
+ generator.state.generatedBlocks++;
}
}
\ No newline at end of file
diff --git a/src/main/java/dev/loki/loparkour/generator/jump/placement/SchematicBlockPlacer.java b/src/main/java/dev/loki/loparkour/generator/jump/placement/SchematicBlockPlacer.java
index d5befc0..00638f2 100644
--- a/src/main/java/dev/loki/loparkour/generator/jump/placement/SchematicBlockPlacer.java
+++ b/src/main/java/dev/loki/loparkour/generator/jump/placement/SchematicBlockPlacer.java
@@ -53,6 +53,7 @@ class SchematicBlockPlacer {
}
generator.state.history.addAll(schematicBlocks);
+ generator.state.generatedBlocks += schematicBlocks.size();
generator.state.schematicBlocks = schematicBlocks;
for (final ParkourPlayer player : generator.getPlayers()) {
diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java
index c61f928..1b22035 100644
--- a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java
+++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleEventManager.java
@@ -10,6 +10,7 @@ import dev.loki.loparkour.reward.core.Rewards;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -90,9 +91,18 @@ public class LifecycleEventManager {
if (!Rewards.ONE_TIME_REWARDS.containsKey(score)) {
return;
}
+ if (player.collectedRewards == null) {
+ player.collectedRewards = new ArrayList<>();
+ }
+ final String rewardKey = String.valueOf(score);
+ if (player.collectedRewards.contains(rewardKey)) {
+ return;
+ }
for (final Reward reward : Rewards.ONE_TIME_REWARDS.get(score)) {
reward.execute(player, mode);
}
+ player.collectedRewards.add(rewardKey);
+ player.save(false);
}
private void processFall() {
diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java
index 39ddaf9..7e01e4f 100644
--- a/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java
+++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/loop/LifecycleTickManager.java
@@ -3,6 +3,7 @@ package dev.loki.loparkour.generator.lifecycle.loop;
import dev.loki.loparkour.config.options.Option;
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup;
+import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
/**
@@ -71,7 +72,9 @@ public class LifecycleTickManager {
return;
}
- final double fallThreshold = generator.state.playerSpawn.getY() - FALL_THRESHOLD_OFFSET;
+ final Block latest = generator.getLatest();
+ final double referenceY = latest != null ? latest.getY() : generator.state.playerSpawn.getY();
+ final double fallThreshold = referenceY - FALL_THRESHOLD_OFFSET;
for (final var player : generator.getPlayers()) {
if (player.player.getLocation().getY() < fallThreshold) {
@@ -89,7 +92,7 @@ public class LifecycleTickManager {
}
private void maintainBlockLead() {
- final int currentBlocks = generator.state.history.size();
+ final int currentBlocks = generator.state.generatedBlocks;
final int targetBlocks = generator.state.score + GENERATION_LEAD;
if (currentBlocks < targetBlocks) {
diff --git a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java
index cc1964a..96afe5f 100644
--- a/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java
+++ b/src/main/java/dev/loki/loparkour/generator/lifecycle/player/GeneratorCleanup.java
@@ -122,6 +122,7 @@ public class GeneratorCleanup {
// Reset state
generator.state.history.clear();
+ generator.state.generatedBlocks = 0;
generator.state.score = 0;
generator.state.start = null;
diff --git a/src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java b/src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java
index 7f119fd..bb08599 100644
--- a/src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java
+++ b/src/main/java/dev/loki/loparkour/leaderboard/core/LeaderboardSorter.java
@@ -58,7 +58,7 @@ public class LeaderboardSorter {
return compareScore(one, two);
}
case TIME -> {
- return Integer.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis());
+ return Long.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis());
}
case DIFFICULTY -> {
return compareDifficulty(one.getValue().difficulty(), two.getValue().difficulty());
@@ -74,8 +74,8 @@ public class LeaderboardSorter {
if (scoreComparison != 0) {
return scoreComparison;
}
- // Use Integer.compare to avoid overflow
- return Integer.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis());
+ // Use Long.compare to avoid overflow
+ return Long.compare(one.getValue().getTimeMillis(), two.getValue().getTimeMillis());
}
private static int compareDifficulty(String diff1, String diff2) {
diff --git a/src/main/java/dev/loki/loparkour/leaderboard/model/Score.java b/src/main/java/dev/loki/loparkour/leaderboard/model/Score.java
index 55df2b7..65470a8 100644
--- a/src/main/java/dev/loki/loparkour/leaderboard/model/Score.java
+++ b/src/main/java/dev/loki/loparkour/leaderboard/model/Score.java
@@ -12,9 +12,14 @@ public record Score(String name, String time, String difficulty, int score) {
private static final int SCORE_PARTS_COUNT = 4;
private static final int SCORE_INDEX = 3;
- private static final int TIME_PARTS_COUNT = 3;
- private static final int SECONDS_PER_MINUTE = 60;
- private static final int MILLIS_PER_SECOND = 1000;
+ private static final int TIME_SEGMENTS_MIN = 2;
+ private static final int TIME_SEGMENTS_MAX = 3;
+ private static final int MILLIS_SEGMENT_COUNT = 2;
+ private static final long SECONDS_PER_MINUTE = 60L;
+ private static final long SECONDS_PER_HOUR = 3600L;
+ private static final long MILLIS_PER_SECOND = 1000L;
+ private static final String UNKNOWN_TIME = "?";
+ private static final long UNKNOWN_TIME_MILLIS = Long.MAX_VALUE;
/**
* Gets a {@link Score} instance from a string
@@ -39,27 +44,49 @@ public record Score(String name, String time, String difficulty, int score) {
/**
* @return This score's time in millis.
*/
- public int getTimeMillis() {
- if ("?".equals(time)) {
- return Integer.MAX_VALUE; // Unknown time sorts last
+ public long getTimeMillis() {
+ if (UNKNOWN_TIME.equals(time)) {
+ return UNKNOWN_TIME_MILLIS; // Unknown time sorts last
}
try {
- final String[] split = time.split(":");
- if (split.length != TIME_PARTS_COUNT) {
- return Integer.MAX_VALUE;
- }
-
- final int m = Integer.parseInt(split[0]);
- final int s = Integer.parseInt(split[1]);
- final int ms = Integer.parseInt(split[2]);
-
- return m * SECONDS_PER_MINUTE * MILLIS_PER_SECOND + s * MILLIS_PER_SECOND + ms;
+ return parseTime(time);
} catch (NumberFormatException e) {
- return Integer.MAX_VALUE;
+ return UNKNOWN_TIME_MILLIS;
}
}
+ private static long parseTime(String value) {
+ final String[] segments = value.split(":");
+ if (segments.length < TIME_SEGMENTS_MIN || segments.length > TIME_SEGMENTS_MAX) {
+ return UNKNOWN_TIME_MILLIS;
+ }
+
+ final String[] secondsAndMillis = segments[segments.length - 1].split("\\.");
+ if (secondsAndMillis.length > MILLIS_SEGMENT_COUNT) {
+ return UNKNOWN_TIME_MILLIS;
+ }
+
+ return fullSeconds(segments, secondsAndMillis) * MILLIS_PER_SECOND + millis(secondsAndMillis);
+ }
+
+ private static long fullSeconds(String[] segments, String[] secondsAndMillis) {
+ final long seconds = Long.parseLong(secondsAndMillis[0]);
+ if (segments.length == TIME_SEGMENTS_MAX) {
+ return Long.parseLong(segments[0]) * SECONDS_PER_HOUR
+ + Long.parseLong(segments[1]) * SECONDS_PER_MINUTE
+ + seconds;
+ }
+ return Long.parseLong(segments[0]) * SECONDS_PER_MINUTE + seconds;
+ }
+
+ private static long millis(String[] secondsAndMillis) {
+ if (secondsAndMillis.length == MILLIS_SEGMENT_COUNT) {
+ return Long.parseLong(secondsAndMillis[1]);
+ }
+ return 0L;
+ }
+
@Override
public String toString() {
return String.format("%s,%s,%s,%s", name, time, difficulty, score);
diff --git a/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java b/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java
index 4622cdb..4779ced 100644
--- a/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java
+++ b/src/main/java/dev/loki/loparkour/mode/impl/multi/CoopMode.java
@@ -114,7 +114,7 @@ public class CoopMode implements MultiMode {
if (pp == null) {
return;
}
- ParkourUser.leave(pp);
+ session.removePlayers(pp);
}
@Override
diff --git a/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java b/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java
index 85a73c0..bea588a 100644
--- a/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java
+++ b/src/main/java/dev/loki/loparkour/player/core/ParkourUser.java
@@ -70,6 +70,8 @@ public abstract class ParkourUser {
public final Player player;
public final Instant joined;
private ScoreboardManager scoreboardManager;
+ /** True while unregister is in progress; re-entrant unregister calls become no-ops. */
+ public boolean unregistering;
@SuppressWarnings("deprecation") // Scoreboard: legacy registerNewObjective(String,String,String)
public ParkourUser(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
diff --git a/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java b/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java
index 4fcaa09..311b872 100644
--- a/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java
+++ b/src/main/java/dev/loki/loparkour/player/service/UserRegistry.java
@@ -61,23 +61,32 @@ public final class UserRegistry {
}
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
- dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.unloadPlayer(user.getUUID());
- new ParkourLeaveEvent(user).call();
-
- try {
- user.unregister();
- resetScoreboard(user);
- } catch (Exception ex) {
- handleUnregisterError(user, ex);
- }
-
- if (restorePreviousData && shouldKickToBungee(kickIfBungee)) {
- BungeeUtil.sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
+ if (user.unregistering) {
return;
}
- if (restorePreviousData) {
- restorePreviousState(user, urgent);
+ user.unregistering = true;
+ try {
+ dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.unloadPlayer(user.getUUID());
+ new ParkourLeaveEvent(user).call();
+
+ try {
+ user.unregister();
+ resetScoreboard(user);
+ } catch (Exception ex) {
+ handleUnregisterError(user, ex);
+ }
+
+ if (restorePreviousData && shouldKickToBungee(kickIfBungee)) {
+ BungeeUtil.sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
+ return;
+ }
+
+ if (restorePreviousData) {
+ restorePreviousState(user, urgent);
+ }
+ } finally {
+ user.unregistering = false;
}
}
diff --git a/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java b/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java
index 2bbbc65..8102835 100644
--- a/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java
+++ b/src/main/java/dev/loki/loparkour/schematic/core/ParkourSchematic.java
@@ -1,12 +1,14 @@
package dev.loki.loparkour.schematic.core;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import com.sk89q.worldedit.math.BlockVector3;
import dev.loki.loparkour.schematic.nbt.StructurePaster;
import dev.loki.loparkour.schematic.schem.SchemPaster;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.structure.Structure;
+import org.bukkit.util.BlockVector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -17,6 +19,9 @@ import java.util.List;
*/
public final class ParkourSchematic {
+ static final int MAX_SCHEMATIC_EDGE = 128;
+ static final int MAX_SCHEMATIC_BLOCKS = 100_000;
+
private final String id;
private final double difficulty;
private final SchematicFormat format;
@@ -49,6 +54,35 @@ public final class ParkourSchematic {
return new ParkourSchematic(id, difficulty, SchematicFormat.SCHEM, null, clipboard);
}
+ /**
+ * Rejects sizes that are non-positive, exceed the max edge length, or exceed the max block count.
+ */
+ static boolean isSaneSize(double width, double height, double length) {
+ if (width <= 0 || height <= 0 || length <= 0) {
+ return false;
+ }
+ if (width > MAX_SCHEMATIC_EDGE || height > MAX_SCHEMATIC_EDGE || length > MAX_SCHEMATIC_EDGE) {
+ return false;
+ }
+ return width * height * length <= MAX_SCHEMATIC_BLOCKS;
+ }
+
+ static boolean isSaneSize(@Nullable Structure structure) {
+ if (structure == null) {
+ return false;
+ }
+ final BlockVector size = structure.getSize();
+ return isSaneSize(size.getBlockX(), size.getBlockY(), size.getBlockZ());
+ }
+
+ static boolean isSaneSize(@Nullable Clipboard clipboard) {
+ if (clipboard == null || clipboard.getRegion() == null) {
+ return false;
+ }
+ final BlockVector3 size = clipboard.getDimensions();
+ return isSaneSize(size.x(), size.y(), size.z());
+ }
+
@NotNull
public String getId() {
return id;
diff --git a/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java b/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java
index 2e591b4..703d5e8 100644
--- a/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java
+++ b/src/main/java/dev/loki/loparkour/schematic/core/SchematicManager.java
@@ -22,8 +22,8 @@ import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Level;
/**
- * Loads {@code .nbt}, {@code .schem}, and {@code .schematic} files from {@code plugins/LoParkour/schematics/}.
- * Difficulty per structure is configured in {@code schematics/schematics.yml}.
+ * Loads {@code .nbt}, {@code .schem}, and {@code .schematic} files from the schematics folder;
+ * difficulty per structure is configured in {@code schematics/schematics.yml}.
*/
public final class SchematicManager {
@@ -92,7 +92,6 @@ public final class SchematicManager {
if (files == null || files.length == 0) {
return 0;
}
-
final StructureManager structureManager = Compat.getStructureManager();
if (structureManager == null) {
return 0;
@@ -102,11 +101,17 @@ public final class SchematicManager {
final String stem = stem(file, ".nbt");
try {
final Structure structure = structureManager.loadStructure(file);
+ if (!ParkourSchematic.isSaneSize(structure)) {
+ LoParkour.getPlugin().getLogger().warning("Skipping " + file.getName() + ": invalid structure size");
+ continue;
+ }
register(stem, ParkourSchematic.fromNbt(stem, resolveDifficulty(stem), structure));
ok++;
} catch (IOException e) {
- LoParkour.getPlugin().getLogger().severe("Failed to load structure: " + file.getName());
- LoParkour.getPlugin().getLogger().log(Level.SEVERE, "Detailed cause:", e);
+ logLoadFailure("structure", file, e);
+ } catch (RuntimeException e) {
+ // broad catch: corrupted schematics throw unchecked exceptions; never break plugin enable
+ logLoadFailure("structure", file, e);
}
}
return ok;
@@ -120,7 +125,6 @@ public final class SchematicManager {
if (files == null || files.length == 0) {
return 0;
}
-
int ok = 0;
for (final File file : files) {
final String stem = stem(file, file.getName().toLowerCase(Locale.ROOT).endsWith(".schematic")
@@ -128,11 +132,17 @@ public final class SchematicManager {
: ".schem");
try {
final Clipboard clipboard = SchemLoader.load(file);
+ if (!ParkourSchematic.isSaneSize(clipboard)) {
+ LoParkour.getPlugin().getLogger().warning("Skipping " + file.getName() + ": invalid clipboard size");
+ continue;
+ }
register(stem, ParkourSchematic.fromSchem(stem, resolveDifficulty(stem), clipboard));
ok++;
} catch (IOException e) {
- LoParkour.getPlugin().getLogger().severe("Failed to load schematic: " + file.getName());
- LoParkour.getPlugin().getLogger().log(Level.SEVERE, "Detailed cause:", e);
+ logLoadFailure("schematic", file, e);
+ } catch (RuntimeException e) {
+ // broad catch: corrupted schematics throw unchecked exceptions; never break plugin enable
+ logLoadFailure("schematic", file, e);
}
}
return ok;
@@ -149,6 +159,11 @@ public final class SchematicManager {
loaded.put(key, schematic);
}
+ private static void logLoadFailure(@NotNull String kind, @NotNull File file, @NotNull Throwable cause) {
+ LoParkour.getPlugin().getLogger().severe("Failed to load " + kind + ": " + file.getName());
+ LoParkour.getPlugin().getLogger().log(Level.SEVERE, "Detailed cause:", cause);
+ }
+
@NotNull
private static String stem(@NotNull File file, @NotNull String suffix) {
final String name = file.getName();
diff --git a/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java b/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java
index d899d7b..f1b183d 100644
--- a/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java
+++ b/src/main/java/dev/loki/loparkour/schematic/schem/SchemLoader.java
@@ -12,18 +12,27 @@ import java.io.IOException;
public final class SchemLoader {
+ private static final int MAX_FILE_SIZE = 10 * 1024 * 1024;
+
private SchemLoader() {
}
@NotNull
public static Clipboard load(@NotNull File file) throws IOException {
+ if (file.length() > MAX_FILE_SIZE) {
+ throw new IOException("Schematic file too large: " + file.getName());
+ }
final ClipboardFormat format = ClipboardFormats.findByFile(file);
if (format == null) {
throw new IOException("Unsupported schematic file: " + file.getName());
}
try (FileInputStream in = new FileInputStream(file);
ClipboardReader reader = format.getReader(in)) {
- return reader.read();
+ final Clipboard clipboard = reader.read();
+ if (clipboard == null) {
+ throw new IOException("Schematic file returned no clipboard: " + file.getName());
+ }
+ return clipboard;
}
}
}
diff --git a/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java
index cdad28d..6cdd92b 100644
--- a/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java
+++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLConnectionManager.java
@@ -19,6 +19,7 @@ public class SQLConnectionManager {
private static final int MAX_POOL_SIZE = 10;
private static final int CONNECTION_TIMEOUT_MILLIS = 5000;
private static final int MAX_CONNECTION_LIFETIME_MILLIS = 1_800_000;
+ private static final int INITIALIZATION_FAIL_TIMEOUT = -1;
private volatile boolean connected = false;
private final List onConnectCallbacks = new CopyOnWriteArrayList<>();
@@ -47,6 +48,7 @@ public class SQLConnectionManager {
config.setMinimumIdle(2);
config.setConnectionTimeout(CONNECTION_TIMEOUT_MILLIS);
config.setMaxLifetime(MAX_CONNECTION_LIFETIME_MILLIS);
+ config.setInitializationFailTimeout(INITIALIZATION_FAIL_TIMEOUT);
config.setPoolName("LoParkour");
dataSource = new HikariDataSource(config);
diff --git a/src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java b/src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java
index 3553685..fe5c5a5 100644
--- a/src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java
+++ b/src/main/java/dev/loki/loparkour/storage/sql/SQLDataMapper.java
@@ -1,10 +1,13 @@
package dev.loki.loparkour.storage.sql;
+import com.google.gson.reflect.TypeToken;
+import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.leaderboard.model.Score;
import dev.loki.loparkour.player.core.ParkourPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Type;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -107,19 +110,49 @@ public class SQLDataMapper {
// Apply settings if available
if (data.settings() != null && !data.settings().isEmpty()) {
deserializeSettings(player, data.settings());
+ } else {
+ player.setSettings(new HashMap<>());
}
}
@NotNull
private static String serializeSettings(@NotNull ParkourPlayer player) {
- // Simplified settings serialization
- // In real implementation, would serialize player settings to JSON
- return "{}";
+ final Map settings = new HashMap<>();
+ settings.put("style", player.style);
+ settings.put("blockLead", player.blockLead);
+ settings.put("useParticles", player.particles);
+ settings.put("useSpecial", player.useSpecialBlocks);
+ settings.put("showFallMsg", player.showFallMessage);
+ settings.put("showScoreboard", player.showScoreboard);
+ settings.put("selectedTime", player.selectedTime);
+ settings.put("collectedRewards", player.collectedRewards);
+ settings.put("locale", player.locale);
+ settings.put("schematicDifficulty", player.schematicDifficulty);
+ settings.put("sound", player.sound);
+ return LoParkour.getGson().toJson(settings);
}
private static void deserializeSettings(@NotNull ParkourPlayer player, @NotNull String settings) {
- // Simplified settings deserialization
- // In real implementation, would deserialize JSON to player settings
+ final Type type = new TypeToken