fix all migrate 80%
This commit is contained in:
parent
d1dcf6ab57
commit
574f9fc4b5
21 changed files with 1054 additions and 1154 deletions
|
|
@ -57,30 +57,32 @@ public final class GenerationOptions {
|
|||
|
||||
static void init() {
|
||||
// Jump validation
|
||||
JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||
MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance");
|
||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal");
|
||||
MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up");
|
||||
MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down");
|
||||
JUMP_VALIDATION_ENABLED = Config.CONFIG.isPath("jump-validation.enabled") && Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||
MAX_JUMP_DISTANCE = Config.CONFIG.isPath("jump-validation.max-distance") ? Config.CONFIG.getDouble("jump-validation.max-distance") : 5.0;
|
||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.isPath("jump-validation.max-horizontal") ? Config.CONFIG.getDouble("jump-validation.max-horizontal") : 4.0;
|
||||
MAX_VERTICAL_UP = Config.CONFIG.isPath("jump-validation.max-vertical-up") ? Config.CONFIG.getDouble("jump-validation.max-vertical-up") : 1.0;
|
||||
MAX_VERTICAL_DOWN = Config.CONFIG.isPath("jump-validation.max-vertical-down") ? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : 3.0;
|
||||
|
||||
// Jump types
|
||||
JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled");
|
||||
JUMP_TYPES_ENABLED = Config.CONFIG.isPath("jump-types.enabled") && Config.CONFIG.getBoolean("jump-types.enabled");
|
||||
JUMP_TYPE_ENABLED = new java.util.HashMap<>();
|
||||
JUMP_TYPE_CHANCE = new java.util.HashMap<>();
|
||||
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
|
||||
String path = "jump-types.types." + type;
|
||||
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled"));
|
||||
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance"));
|
||||
if (Config.CONFIG.isPath("jump-types.types")) {
|
||||
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
|
||||
String path = "jump-types.types." + type;
|
||||
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.isPath(path + ".enabled") && Config.CONFIG.getBoolean(path + ".enabled"));
|
||||
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.isPath(path + ".chance") ? Config.CONFIG.getDouble(path + ".chance") : 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Memory
|
||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance");
|
||||
CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval");
|
||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance") ? Config.CONFIG.getInt("memory.block-cleanup-distance") : 100;
|
||||
CLEANUP_INTERVAL = Config.CONFIG.isPath("memory.cleanup-interval") ? Config.CONFIG.getInt("memory.cleanup-interval") : 100;
|
||||
|
||||
// Ghost
|
||||
GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled");
|
||||
GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top");
|
||||
GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency");
|
||||
GHOST_MODE_ENABLED = Config.CONFIG.isPath("ghost-mode.enabled") && Config.CONFIG.getBoolean("ghost-mode.enabled");
|
||||
GHOST_SHOW_TOP = Config.CONFIG.isPath("ghost-mode.show-top") ? Config.CONFIG.getInt("ghost-mode.show-top") : 3;
|
||||
GHOST_TRANSPARENCY = Config.CONFIG.isPath("ghost-mode.transparency") ? Config.CONFIG.getDouble("ghost-mode.transparency") : 0.5;
|
||||
|
||||
// Generation chances
|
||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ public class Option {
|
|||
|
||||
/** @see SqlOptions#SQL */
|
||||
public static boolean SQL;
|
||||
public static String SQL_URL;
|
||||
public static int SQL_PORT;
|
||||
public static String SQL_DB;
|
||||
public static String SQL_USERNAME;
|
||||
public static String SQL_PASSWORD;
|
||||
public static String SQL_PREFIX;
|
||||
|
||||
/** @see ParticleOptions#SHAPE */
|
||||
public static ParticleShape PARTICLE_SHAPE;
|
||||
|
|
@ -87,7 +93,13 @@ public class Option {
|
|||
|
||||
/** Copy sub-class fields to this class so existing code still compiles. */
|
||||
private static void syncForwardedFields() {
|
||||
SQL = SqlOptions.SQL;
|
||||
SQL = SqlOptions.SQL;
|
||||
SQL_URL = SqlOptions.URL;
|
||||
SQL_PORT = SqlOptions.PORT;
|
||||
SQL_DB = SqlOptions.DB;
|
||||
SQL_USERNAME = SqlOptions.USERNAME;
|
||||
SQL_PASSWORD = SqlOptions.PASSWORD;
|
||||
SQL_PREFIX = SqlOptions.PREFIX;
|
||||
|
||||
PARTICLE_SHAPE = ParticleOptions.SHAPE;
|
||||
SOUND_TYPE = ParticleOptions.SOUND_TYPE;
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ public class BlockPlacer {
|
|||
|
||||
/** Generates one block or schematic ahead. */
|
||||
public void generate() {
|
||||
if (g.waitForSchematicCompletion) return;
|
||||
if (g.state.waitForSchematicCompletion) return;
|
||||
|
||||
Map<ParkourGenerator.BlockGenerationType, Double> chances = new HashMap<>(g.defaultChances);
|
||||
if (g.schematicCooldown > 0
|
||||
Map<ParkourGenerator.BlockGenerationType, Double> chances = new HashMap<>(g.state.defaultChances);
|
||||
if (g.state.schematicCooldown > 0
|
||||
|| g.generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS)
|
||||
|| g.profile.get("schematicDifficulty").asDouble() == 0.0
|
||||
|| !g.schematicBlocks.isEmpty()) {
|
||||
|| !g.state.schematicBlocks.isEmpty()) {
|
||||
chances.remove(ParkourGenerator.BlockGenerationType.SCHEMATIC);
|
||||
}
|
||||
if (!g.profile.get("useSpecialBlocks").asBoolean()) {
|
||||
|
|
@ -67,27 +67,27 @@ public class BlockPlacer {
|
|||
|
||||
/** Seeds the history and generates the initial lead. */
|
||||
public void generateFirst(Location spawn, Location blockSpawn) {
|
||||
g.playerSpawn = spawn;
|
||||
g.lastStandingPlayerLocation = spawn;
|
||||
g.blockSpawn = blockSpawn;
|
||||
g.history.add(blockSpawn.getBlock());
|
||||
g.state.playerSpawn = spawn;
|
||||
g.state.lastStandingPlayerLocation = spawn;
|
||||
g.state.blockSpawn = blockSpawn;
|
||||
g.state.history.add(blockSpawn.getBlock());
|
||||
generate(g.profile.get("blockLead").asInt());
|
||||
}
|
||||
|
||||
/** Clears schematic blocks and resets cooldown. */
|
||||
public void deleteSchematic() {
|
||||
if (!g.deleteSchematic) return;
|
||||
g.schematicBlocks.forEach(b -> b.setType(Material.AIR));
|
||||
g.schematicBlocks.clear();
|
||||
g.deleteSchematic = false;
|
||||
g.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
if (!g.state.deleteSchematic) return;
|
||||
g.state.schematicBlocks.forEach(b -> b.setType(Material.AIR));
|
||||
g.state.schematicBlocks.clear();
|
||||
g.state.deleteSchematic = false;
|
||||
g.state.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
}
|
||||
|
||||
// ── Block selection ────────────────────────────────────────────────────────
|
||||
|
||||
protected List<Block> selectBlocks() {
|
||||
int height = Probs.random(g.heightChances);
|
||||
int distance = Probs.random(g.distanceChances);
|
||||
int height = Probs.random(g.state.heightChances);
|
||||
int distance = Probs.random(g.state.distanceChances);
|
||||
return List.of(selectNext(g.getLatest(), distance, height));
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ public class BlockPlacer {
|
|||
JumpDirector director = new JumpDirector(
|
||||
BoundingBox.of(g.zone[0], g.zone[1]), g.getLatest().getLocation().toVector());
|
||||
|
||||
g.heading = director.getRecommendedHeading(g.heading);
|
||||
g.state.heading = director.getRecommendedHeading(g.state.heading);
|
||||
height = director.getRecommendedHeight(height);
|
||||
|
||||
switch (g.getLatest().getType()) {
|
||||
|
|
@ -107,11 +107,11 @@ public class BlockPlacer {
|
|||
double sd = g.generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
|
||||
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd);
|
||||
|
||||
Vector offset = g.heading.clone().multiply(distance + 1).setY(height);
|
||||
Vector offset = g.state.heading.clone().multiply(distance + 1).setY(height);
|
||||
if (offset.getX() == 0) offset.setX(randomOffset);
|
||||
else offset.setZ(randomOffset);
|
||||
|
||||
offset.rotateAroundY(angleInY(g.heading, Option.HEADING.getDirection()));
|
||||
offset.rotateAroundY(angleInY(g.state.heading, Option.HEADING.getDirection()));
|
||||
return current.getLocation().add(offset).getBlock();
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ public class BlockPlacer {
|
|||
for (Block block : blocks) {
|
||||
BlockData data = (jump == ParkourGenerator.BlockGenerationType.SPECIAL
|
||||
&& !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL))
|
||||
? Probs.random(g.specialChances)
|
||||
? Probs.random(g.state.specialChances)
|
||||
: selectBlockData();
|
||||
|
||||
if (data instanceof Fence) block = block.getLocation().subtract(0, 1, 0).getBlock();
|
||||
|
|
@ -148,8 +148,8 @@ public class BlockPlacer {
|
|||
new ParkourBlockGenerateEvent(placed, g, g.player).call();
|
||||
g.effects.particles(placed);
|
||||
g.effects.sound(placed);
|
||||
g.history.addAll(placed);
|
||||
g.schematicCooldown--;
|
||||
g.state.history.addAll(placed);
|
||||
g.state.schematicCooldown--;
|
||||
}
|
||||
|
||||
private boolean tryGenerateSchematic() {
|
||||
|
|
@ -166,14 +166,14 @@ public class BlockPlacer {
|
|||
var pool = candidates.isEmpty() ? new ArrayList<>(all.values()) : candidates;
|
||||
var schematic = pool.get(new java.util.Random().nextInt(pool.size()));
|
||||
|
||||
Location origin = g.getLatest().getLocation().add(g.heading.clone().multiply(2));
|
||||
Location origin = g.getLatest().getLocation().add(g.state.heading.clone().multiply(2));
|
||||
List<Block> placed = rotatedPaste(schematic, origin);
|
||||
if (placed.isEmpty()) return false;
|
||||
|
||||
g.schematicBlocks.addAll(placed);
|
||||
g.history.addAll(placed);
|
||||
g.waitForSchematicCompletion = true;
|
||||
g.deleteSchematic = false;
|
||||
g.state.schematicBlocks.addAll(placed);
|
||||
g.state.history.addAll(placed);
|
||||
g.state.waitForSchematicCompletion = true;
|
||||
g.state.deleteSchematic = false;
|
||||
|
||||
new ParkourSchematicGenerateEvent(schematic, g, g.player).call();
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,120 +1,7 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.util.Probs;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Selects next parkour blocks (SRP - Single Responsibility)
|
||||
* @deprecated Logic merged into {@link BlockPlacer}. Delete this file.
|
||||
*/
|
||||
public class BlockSelector {
|
||||
|
||||
private final Profile profile;
|
||||
private final Location[] zone;
|
||||
private final Vector initialHeading;
|
||||
private final Map<Integer, Double> heightChances = new HashMap<>();
|
||||
private final Map<Integer, Double> distanceChances = new HashMap<>();
|
||||
|
||||
private Vector heading;
|
||||
|
||||
public BlockSelector(Profile profile, Location[] zone, Vector initialHeading) {
|
||||
this.profile = profile;
|
||||
this.zone = zone;
|
||||
this.heading = initialHeading.clone();
|
||||
this.initialHeading = initialHeading.clone();
|
||||
|
||||
calculateChances();
|
||||
}
|
||||
|
||||
private void calculateChances() {
|
||||
heightChances.put(1, Option.NORMAL_HEIGHT_1);
|
||||
heightChances.put(0, Option.NORMAL_HEIGHT_0);
|
||||
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
||||
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
||||
|
||||
distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
||||
distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
||||
distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
||||
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
||||
}
|
||||
|
||||
public List<Block> selectBlocks(Block latest) {
|
||||
int height = Probs.random(heightChances);
|
||||
int distance = Probs.random(distanceChances);
|
||||
return List.of(selectNext(latest, distance, height));
|
||||
}
|
||||
|
||||
private Block selectNext(Block current, int distance, int height) {
|
||||
JumpDirector director = new JumpDirector(
|
||||
BoundingBox.of(zone[0], zone[1]),
|
||||
current.getLocation().toVector()
|
||||
);
|
||||
|
||||
heading = director.getRecommendedHeading(heading);
|
||||
height = director.getRecommendedHeight(height);
|
||||
|
||||
// Ensure special blocks are possible
|
||||
switch (current.getType()) {
|
||||
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
|
||||
case GLASS_PANE -> distance = Math.min(distance, 3);
|
||||
}
|
||||
|
||||
if (height > 0) {
|
||||
distance = Math.max(distance - height, 1);
|
||||
}
|
||||
|
||||
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, 1);
|
||||
|
||||
Vector offset = heading.clone()
|
||||
.multiply(distance + 1)
|
||||
.setY(height);
|
||||
|
||||
if (offset.getX() == 0) {
|
||||
offset.setX(randomOffset);
|
||||
} else {
|
||||
offset.setZ(randomOffset);
|
||||
}
|
||||
|
||||
offset.rotateAroundY(angleInY(heading, initialHeading));
|
||||
|
||||
return current.getLocation().add(offset).getBlock();
|
||||
}
|
||||
|
||||
public BlockData selectBlockData() {
|
||||
Style style = Registry.getStyle(profile.get("style").value());
|
||||
|
||||
if (style == null) {
|
||||
profile.set("style", Registry.getStyles().stream()
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getName());
|
||||
return selectBlockData();
|
||||
}
|
||||
|
||||
return style.getNext().createBlockData();
|
||||
}
|
||||
|
||||
private double angleInY(Vector a, Vector b) {
|
||||
double det = a.getX() * b.getZ() - a.getZ() * b.getX();
|
||||
return Math.atan2(det, a.dot(b));
|
||||
}
|
||||
|
||||
public Vector getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public void resetHeading() {
|
||||
heading = initialHeading.clone();
|
||||
}
|
||||
}
|
||||
@Deprecated
|
||||
class BlockSelector {}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public final class Island {
|
|||
|
||||
// First parkour block should be generated FROM the parkour start block
|
||||
// in the direction of heading, not AT the parkour start block
|
||||
Location parkourStart = parkour.getLocation().add(session.generator.heading);
|
||||
Location parkourStart = parkour.getLocation().add(session.generator.state.heading);
|
||||
|
||||
session.generator.generateFirst(ps, parkourStart);
|
||||
session.generator.startTick();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator;
|
||||
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.api.event.ParkourFallEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
|
|
@ -14,15 +10,12 @@ import dev.loki.loparkour.mode.Mode;
|
|||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
|
@ -31,61 +24,23 @@ import java.time.ZoneOffset;
|
|||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Coordinates parkour generation, ticking, scoring, and reset.
|
||||
*
|
||||
* Heavy lifting is delegated to:
|
||||
* <ul>
|
||||
* <li>{@link BlockPlacer} – block selection & placement</li>
|
||||
* <li>{@link EffectManager} – particles & sounds</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ParkourGenerator {
|
||||
|
||||
public static final int BLOCK_TRAIL = 2;
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────────
|
||||
public int score = 0;
|
||||
public int totalScore = 0;
|
||||
public int schematicCooldown;
|
||||
public boolean stopped = false;
|
||||
|
||||
public Location[] zone;
|
||||
public ParkourPlayer player;
|
||||
public dev.lolib.scheduler.ScheduledTask task;
|
||||
private dev.lolib.scheduler.ScheduledTask cleanupTask;
|
||||
|
||||
public Location blockSpawn;
|
||||
public Location playerSpawn;
|
||||
public Instant start;
|
||||
public Vector heading = Option.HEADING.getDirection();
|
||||
|
||||
public final List<GeneratorOption> generatorOptions;
|
||||
public final Session session;
|
||||
public final Profile profile = new Profile();
|
||||
public final Island island;
|
||||
|
||||
// Chance maps – populated by calculateChances()
|
||||
public final Map<Integer, Double> distanceChances = new HashMap<>();
|
||||
public final Map<Integer, Double> heightChances = new HashMap<>();
|
||||
public final Map<BlockData, Double> specialChances = new HashMap<>();
|
||||
public final Map<BlockGenerationType, Double> defaultChances = new HashMap<>();
|
||||
|
||||
// Schematic state
|
||||
public boolean deleteSchematic = false;
|
||||
public boolean waitForSchematicCompletion = false;
|
||||
public List<Block> schematicBlocks = new ArrayList<>();
|
||||
|
||||
// History
|
||||
public List<Block> history = new LinkedList<>();
|
||||
public int lastPositionIndexPlayer = -1;
|
||||
public Location lastStandingPlayerLocation;
|
||||
|
||||
// Delegates
|
||||
public final GeneratorState state = new GeneratorState();
|
||||
public final GeneratorLifecycle lifecycle;
|
||||
public final EffectManager effects;
|
||||
public final BlockPlacer placer;
|
||||
|
||||
// ── Constructors ───────────────────────────────────────────────────────────
|
||||
public final Session session;
|
||||
public final ParkourPlayer player;
|
||||
public final Profile profile = new Profile();
|
||||
public final Island island;
|
||||
public final List<GeneratorOption> generatorOptions;
|
||||
|
||||
public Location[] zone;
|
||||
public dev.lolib.scheduler.ScheduledTask task;
|
||||
|
||||
public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic,
|
||||
GeneratorOption... generatorOptions) {
|
||||
|
|
@ -94,9 +49,13 @@ public class ParkourGenerator {
|
|||
this.player = session.getPlayers().get(0);
|
||||
this.island = new Island(session, schematic);
|
||||
this.zone = Divider.toSelection(session);
|
||||
this.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
this.state.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
this.state.heading = Option.HEADING.getDirection();
|
||||
|
||||
this.effects = new EffectManager(this);
|
||||
this.placer = new BlockPlacer(this);
|
||||
this.placer = new BlockPlacer(this);
|
||||
this.lifecycle = new GeneratorLifecycle(this);
|
||||
|
||||
calculateChances();
|
||||
}
|
||||
|
||||
|
|
@ -110,211 +69,100 @@ public class ParkourGenerator {
|
|||
var manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) return null;
|
||||
LPSchematic s = manager.getSchematic(name);
|
||||
if (s == null) LoParkour.getPlugin().getLogger()
|
||||
.warning("Island schematic '%s' not found!".formatted(name));
|
||||
if (s == null) {
|
||||
LoParkour.getPlugin().getLogger().warning("Island schematic '%s' not found!".formatted(name));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public void overrideProfile() {}
|
||||
|
||||
// ── Chances ────────────────────────────────────────────────────────────────
|
||||
|
||||
protected void calculateChances() {
|
||||
defaultChances.clear();
|
||||
defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
||||
defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
||||
defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
||||
state.defaultChances.clear();
|
||||
state.defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
||||
state.defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
||||
state.defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
||||
|
||||
heightChances.clear();
|
||||
heightChances.put( 1, Option.NORMAL_HEIGHT_1);
|
||||
heightChances.put( 0, Option.NORMAL_HEIGHT_0);
|
||||
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
||||
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
||||
state.heightChances.clear();
|
||||
state.heightChances.put(1, Option.NORMAL_HEIGHT_1);
|
||||
state.heightChances.put(0, Option.NORMAL_HEIGHT_0);
|
||||
state.heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
|
||||
state.heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
|
||||
|
||||
distanceChances.clear();
|
||||
distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
||||
distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
||||
distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
||||
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
||||
state.distanceChances.clear();
|
||||
state.distanceChances.put(1, Option.NORMAL_DISTANCE_1);
|
||||
state.distanceChances.put(2, Option.NORMAL_DISTANCE_2);
|
||||
state.distanceChances.put(3, Option.NORMAL_DISTANCE_3);
|
||||
state.distanceChances.put(4, Option.NORMAL_DISTANCE_4);
|
||||
|
||||
specialChances.clear();
|
||||
specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
|
||||
specialChances.put(Material.SMOOTH_QUARTZ_SLAB.createBlockData("[type=bottom]"), Option.SPECIAL_SLAB);
|
||||
specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
|
||||
specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
|
||||
state.specialChances.clear();
|
||||
state.specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
|
||||
state.specialChances.put(Material.SMOOTH_QUARTZ_SLAB.createBlockData("[type=bottom]"), Option.SPECIAL_SLAB);
|
||||
state.specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
|
||||
state.specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
|
||||
}
|
||||
|
||||
// ── Generation (delegates) ─────────────────────────────────────────────────
|
||||
public void generate() { placer.generate(); }
|
||||
public void generate(int amount) { placer.generate(amount); }
|
||||
public void generateFirst(Location spawn, Location block) { placer.generateFirst(spawn, block); }
|
||||
|
||||
public void generate() { placer.generate(); }
|
||||
public void generate(int amount) { placer.generate(amount); }
|
||||
|
||||
public void generateFirst(Location spawn, Location block) {
|
||||
placer.generateFirst(spawn, block);
|
||||
}
|
||||
|
||||
// ── Tick ───────────────────────────────────────────────────────────────────
|
||||
|
||||
public void startTick() {
|
||||
task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
|
||||
cleanupTask = Scheduler.get(LoParkour.getPlugin()).runTimer(this::cleanupDistantBlocks, 0, Option.CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
if (stopped) { task.cancel(); if (cleanupTask != null) cleanupTask.cancel(); return; }
|
||||
|
||||
getPlayers().forEach(p -> {
|
||||
updateVisualTime(p, p.selectedTime);
|
||||
p.updateScoreboard(this);
|
||||
p.player.setSaturation(20);
|
||||
});
|
||||
getSpectators().forEach(ParkourSpectator::update);
|
||||
|
||||
if (player.getLocation().getWorld() != lastStandingPlayerLocation.getWorld()) return;
|
||||
if (player.getLocation().subtract(lastStandingPlayerLocation).getY() < -10) { fall(); return; }
|
||||
|
||||
Block below = blockBelow();
|
||||
if (below == null) return;
|
||||
|
||||
// Schematic end block
|
||||
if (schematicBlocks.contains(below) && below.getType() == Material.RED_WOOL && !deleteSchematic) {
|
||||
for (int i = 0; i < profile.get("schematicDifficulty").asDouble() * 15; i++) score();
|
||||
waitForSchematicCompletion = false;
|
||||
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
generate(profile.get("blockLead").asInt());
|
||||
deleteSchematic = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!history.contains(below)) return;
|
||||
|
||||
int idx = history.indexOf(below);
|
||||
int delta = idx - lastPositionIndexPlayer;
|
||||
if (delta <= 0) return;
|
||||
|
||||
lastStandingPlayerLocation = player.getLocation();
|
||||
|
||||
int lead = profile.get("blockLead").asInt();
|
||||
if (history.size() - idx <= lead) generate(lead - (history.size() - idx));
|
||||
lastPositionIndexPlayer = idx;
|
||||
|
||||
// Remove trail blocks behind player
|
||||
for (int i = idx - BLOCK_TRAIL - 1; i >= idx - 4 * BLOCK_TRAIL; i--) {
|
||||
if (i > 0) history.get(i).setType(Material.AIR);
|
||||
}
|
||||
|
||||
cleanupDistantBlocks();
|
||||
placer.deleteSchematic();
|
||||
|
||||
int pts = Config.CONFIG.getBoolean("scoring.all-points") ? delta : 1;
|
||||
for (int i = 0; i < pts; i++) score();
|
||||
|
||||
if (start == null) start = Instant.now();
|
||||
}
|
||||
|
||||
private @Nullable Block blockBelow() {
|
||||
Location loc = player.getLocation().subtract(0, 1, 0);
|
||||
Block b = loc.getBlock();
|
||||
if (b.getType() == Material.AIR) {
|
||||
if (loc.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) return null;
|
||||
b = loc.getBlock();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private void updateVisualTime(ParkourPlayer p, int selectedTime) {
|
||||
int t = 18000 + selectedTime;
|
||||
if (t >= 24000) t -= 24000;
|
||||
p.player.setPlayerTime(t, false);
|
||||
}
|
||||
|
||||
// ── Score ──────────────────────────────────────────────────────────────────
|
||||
|
||||
protected void score() {
|
||||
score++;
|
||||
totalScore++;
|
||||
checkRewards();
|
||||
new ParkourScoreEvent(player).call();
|
||||
}
|
||||
|
||||
private void checkRewards() {
|
||||
if (!Rewards.REWARDS_ENABLED || score == 0) return;
|
||||
|
||||
if (Rewards.SCORE_REWARDS.containsKey(score))
|
||||
Rewards.SCORE_REWARDS.get(score).forEach(r -> r.execute(player, getMode()));
|
||||
|
||||
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
|
||||
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
|
||||
if (intervalScore % interval == 0)
|
||||
Rewards.INTERVAL_REWARDS.get(interval).forEach(r -> r.execute(player, getMode()));
|
||||
}
|
||||
|
||||
String key = Integer.toString(score);
|
||||
if (Rewards.ONE_TIME_REWARDS.containsKey(score) && !player.collectedRewards.contains(key)) {
|
||||
Rewards.ONE_TIME_REWARDS.get(score).forEach(r -> r.execute(player, getMode()));
|
||||
player.collectedRewards.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fall / Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
protected void fall() {
|
||||
new ParkourFallEvent(player).call();
|
||||
reset(true);
|
||||
}
|
||||
public void startTick() { lifecycle.startTick(); }
|
||||
public void tick() { lifecycle.tick(); }
|
||||
protected void fall() { lifecycle.fall(); }
|
||||
protected void score() { lifecycle.score(); }
|
||||
|
||||
public void reset(boolean regenerate) {
|
||||
stopped = !regenerate;
|
||||
|
||||
if (!regenerate && task == null)
|
||||
state.stopped = !regenerate;
|
||||
if (!regenerate && task == null) {
|
||||
LoParkour.getPlugin().getLogger().warning("## Incomplete joining setup — report this!");
|
||||
|
||||
lastPositionIndexPlayer = 0;
|
||||
if (!history.isEmpty()) {
|
||||
history.remove(0);
|
||||
history.forEach(b -> b.setType(Material.AIR, false));
|
||||
history.clear();
|
||||
}
|
||||
|
||||
waitForSchematicCompletion = false;
|
||||
deleteSchematic = true;
|
||||
state.lastPositionIndexPlayer = 0;
|
||||
if (!state.history.isEmpty()) {
|
||||
state.history.remove(0);
|
||||
state.history.forEach(b -> b.setType(Material.AIR, false));
|
||||
state.history.clear();
|
||||
}
|
||||
state.resetSchematicState();
|
||||
placer.deleteSchematic();
|
||||
|
||||
Leaderboard lb = getMode().getLeaderboard();
|
||||
int record = lb != null ? lb.get(player.getUUID()).score() : 0;
|
||||
|
||||
if (profile.get("showFallMessage").asBoolean()) sendFallMessage(record);
|
||||
if (lb != null && score > record)
|
||||
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), score);
|
||||
|
||||
score = 0;
|
||||
start = null;
|
||||
heading = Option.HEADING.getDirection();
|
||||
|
||||
if (profile.get("showFallMessage").asBoolean()) {
|
||||
sendFallMessage(record);
|
||||
}
|
||||
if (lb != null && state.score > record) {
|
||||
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), state.score);
|
||||
}
|
||||
state.resetScore();
|
||||
state.heading = Option.HEADING.getDirection();
|
||||
if (regenerate) {
|
||||
player.teleport(playerSpawn);
|
||||
generateFirst(playerSpawn, blockSpawn);
|
||||
player.teleport(state.playerSpawn);
|
||||
generateFirst(state.playerSpawn, state.blockSpawn);
|
||||
return;
|
||||
}
|
||||
|
||||
island.destroy();
|
||||
if (getPlayers().isEmpty())
|
||||
if (getPlayers().isEmpty()) {
|
||||
getSpectators().forEach(s -> Modes.DEFAULT.create(s.player));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFallMessage(int record) {
|
||||
String key;
|
||||
int number = 0;
|
||||
if (score == record) key = "settings.parkour_settings.items.fall_message.formats.tied";
|
||||
else if (score > record) { key = "settings.parkour_settings.items.fall_message.formats.beat"; number = score - record; }
|
||||
else { key = "settings.parkour_settings.items.fall_message.formats.miss"; number = record - score; }
|
||||
|
||||
if (state.score == record) {
|
||||
key = "settings.parkour_settings.items.fall_message.formats.tied";
|
||||
} else if (state.score > record) {
|
||||
key = "settings.parkour_settings.items.fall_message.formats.beat";
|
||||
number = state.score - record;
|
||||
} else {
|
||||
key = "settings.parkour_settings.items.fall_message.formats.miss";
|
||||
number = record - state.score;
|
||||
}
|
||||
for (ParkourPlayer p : getPlayers()) {
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(score));
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(state.score));
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.high_score", Integer.toString(record));
|
||||
p.sendTranslated(key, Integer.toString(number));
|
||||
p.sendTranslated(key, Integer.toString(number));
|
||||
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
|
||||
}
|
||||
}
|
||||
|
|
@ -325,29 +173,8 @@ public class ParkourGenerator {
|
|||
getPlayers().forEach(p -> lb.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
|
||||
}
|
||||
|
||||
// ── Cleanup ────────────────────────────────────────────────────────────────
|
||||
|
||||
protected void cleanupDistantBlocks() {
|
||||
if (history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) return;
|
||||
Location loc = player.getLocation();
|
||||
int removed = 0;
|
||||
Iterator<Block> it = history.iterator();
|
||||
while (it.hasNext()) {
|
||||
Block b = it.next();
|
||||
if (b.getLocation().distance(loc) > Option.BLOCK_CLEANUP_DISTANCE) {
|
||||
b.setType(Material.AIR, false);
|
||||
it.remove();
|
||||
removed++;
|
||||
} else break;
|
||||
}
|
||||
if (removed > 0) lastPositionIndexPlayer = Math.max(0, lastPositionIndexPlayer - removed);
|
||||
}
|
||||
|
||||
// ── Misc ───────────────────────────────────────────────────────────────────
|
||||
|
||||
public void menu(ParkourPlayer player) { Menus.PARKOUR_SETTINGS.open(player); }
|
||||
|
||||
public Block getLatest() { return history.get(history.size() - 1); }
|
||||
public Block getLatest() { return state.history.get(state.history.size() - 1); }
|
||||
|
||||
public double getDifficultyScore() {
|
||||
double s = 0;
|
||||
|
|
@ -357,11 +184,16 @@ public class ParkourGenerator {
|
|||
return s;
|
||||
}
|
||||
|
||||
public String getFormattedTime() { return getTime(Config.CONFIG.getString("options.time.score-format")); }
|
||||
public String getDetailedTime() { return getTime("mm:ss:SSS"); }
|
||||
public String getFormattedTime() {
|
||||
return getTime(Config.CONFIG.getString("options.time.score-format"));
|
||||
}
|
||||
|
||||
public String getDetailedTime() {
|
||||
return getTime("mm:ss:SSS");
|
||||
}
|
||||
|
||||
private String getTime(String format) {
|
||||
var ms = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
|
||||
var ms = Instant.now().minusMillis(state.start != null ? state.start.toEpochMilli() : Instant.now().toEpochMilli());
|
||||
try {
|
||||
return DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC).format(ms);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
|
|
@ -371,8 +203,7 @@ public class ParkourGenerator {
|
|||
}
|
||||
|
||||
public Mode getMode() { return Modes.DEFAULT; }
|
||||
|
||||
@NotNull public List<ParkourPlayer> getPlayers() { return session.getPlayers(); }
|
||||
@NotNull public List<ParkourPlayer> getPlayers() { return session.getPlayers(); }
|
||||
@NotNull public List<ParkourSpectator> getSpectators() { return session.getSpectators(); }
|
||||
|
||||
public enum BlockGenerationType { DEFAULT, SCHEMATIC, SPECIAL }
|
||||
|
|
|
|||
|
|
@ -1,126 +1,8 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages scoring and rewards (SRP - Single Responsibility)
|
||||
* @deprecated Score logic lives in {@link GeneratorLifecycle}.
|
||||
* This class was created but never wired in. Delete this file.
|
||||
*/
|
||||
public class ScoreManager {
|
||||
|
||||
private final ParkourPlayer player;
|
||||
private final Mode mode;
|
||||
private final Profile profile;
|
||||
|
||||
private int score = 0;
|
||||
private int totalScore = 0;
|
||||
private Instant start;
|
||||
|
||||
public ScoreManager(ParkourPlayer player, Mode mode, Profile profile) {
|
||||
this.player = player;
|
||||
this.mode = mode;
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
public void addScore(int points) {
|
||||
for (int i = 0; i < points; i++) {
|
||||
score++;
|
||||
totalScore++;
|
||||
checkRewards();
|
||||
new ParkourScoreEvent(player).call();
|
||||
}
|
||||
|
||||
if (start == null) {
|
||||
start = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRewards() {
|
||||
if (!Rewards.REWARDS_ENABLED || score == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Rewards.SCORE_REWARDS.containsKey(score)) {
|
||||
Rewards.SCORE_REWARDS.get(score).forEach(s -> s.execute(player, mode));
|
||||
}
|
||||
|
||||
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
|
||||
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
|
||||
if (intervalScore % interval != 0) continue;
|
||||
Rewards.INTERVAL_REWARDS.get(interval).forEach(s -> s.execute(player, mode));
|
||||
}
|
||||
|
||||
if (Rewards.ONE_TIME_REWARDS.containsKey(score) &&
|
||||
!player.collectedRewards.contains(Integer.toString(score))) {
|
||||
Rewards.ONE_TIME_REWARDS.get(score).forEach(s -> s.execute(player, mode));
|
||||
player.collectedRewards.add(Integer.toString(score));
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
score = 0;
|
||||
start = null;
|
||||
}
|
||||
|
||||
public void registerScore(List<ParkourPlayer> players) {
|
||||
Leaderboard leaderboard = mode.getLeaderboard();
|
||||
if (leaderboard == null) return;
|
||||
|
||||
String time = getDetailedTime();
|
||||
String difficulty = Double.toString(getDifficultyScore()).substring(0, Math.min(3, Double.toString(getDifficultyScore()).length()));
|
||||
|
||||
players.forEach(p -> leaderboard.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
|
||||
}
|
||||
|
||||
public String getFormattedTime() {
|
||||
return getTime(Config.CONFIG.getString("options.time.score-format"));
|
||||
}
|
||||
|
||||
public String getDetailedTime() {
|
||||
return getTime("mm:ss:SSS");
|
||||
}
|
||||
|
||||
private String getTime(String format) {
|
||||
var timeMs = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
|
||||
|
||||
try {
|
||||
return DateTimeFormatter.ofPattern(format)
|
||||
.withZone(ZoneOffset.UTC)
|
||||
.format(timeMs);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private double getDifficultyScore() {
|
||||
double score = 0;
|
||||
if (profile.get("useSpecialBlocks").asBoolean()) score += 0.5;
|
||||
|
||||
double schematicDiff = profile.get("schematicDifficulty").asDouble();
|
||||
if (schematicDiff > 0) {
|
||||
if (schematicDiff <= 0.25) score += 0.2;
|
||||
else if (schematicDiff <= 0.5) score += 0.3;
|
||||
else if (schematicDiff <= 0.75) score += 0.4;
|
||||
else score += 0.5;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public int getTotalScore() {
|
||||
return totalScore;
|
||||
}
|
||||
}
|
||||
@Deprecated
|
||||
class ScoreManager {}
|
||||
|
|
|
|||
29
src/main/java/dev/loki/loparkour/player/BungeeUtil.java
Normal file
29
src/main/java/dev/loki/loparkour/player/BungeeUtil.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
|
||||
|
||||
/**
|
||||
* Utility for BungeeCord operations.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class BungeeUtil {
|
||||
|
||||
public static void sendPlayerToServer(Player player, String server) {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Connect");
|
||||
out.writeUTF(server);
|
||||
|
||||
try {
|
||||
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
|
||||
} catch (ChannelNotRegisteredException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error while trying to send %s to server %s. - %s".formatted(player.getName(), server, ex.getMessage()));
|
||||
player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,33 +36,7 @@ import java.util.function.BiConsumer;
|
|||
*/
|
||||
public class ParkourPlayer extends ParkourUser {
|
||||
|
||||
public static final Map<String, OptionContainer> PLAYER_COLUMNS = new HashMap<>();
|
||||
|
||||
static {
|
||||
PLAYER_COLUMNS.put("uuid", new OptionContainer(null, null));
|
||||
PLAYER_COLUMNS.put("style", new OptionContainer(ParkourOption.STYLES, (player, v) -> player.style = v));
|
||||
PLAYER_COLUMNS.put("blockLead", new OptionContainer(ParkourOption.LEADS, (player, v) -> player.blockLead = Integer.parseInt(v)));
|
||||
PLAYER_COLUMNS.put("useParticles", new OptionContainer(ParkourOption.PARTICLES, (player, v) -> player.particles = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("useSpecial", new OptionContainer(ParkourOption.SPECIAL_BLOCKS, (player, v) -> player.useSpecialBlocks = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("showFallMsg", new OptionContainer(ParkourOption.FALL_MESSAGE, (player, v) -> player.showFallMessage = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("showScoreboard", new OptionContainer(ParkourOption.SCOREBOARD, (player, v) -> player.showScoreboard = parseBoolean(v)));
|
||||
PLAYER_COLUMNS.put("selectedTime", new OptionContainer(ParkourOption.TIME, (player, v) -> player.selectedTime = Integer.parseInt(v)));
|
||||
PLAYER_COLUMNS.put("collectedRewards", new OptionContainer(null, (player, v) -> {
|
||||
player.collectedRewards = new ArrayList<>();
|
||||
|
||||
if (!v.isEmpty()) {
|
||||
player.collectedRewards.addAll(Arrays.stream(v.replaceAll("[ \\[\\]]", "").split(","))
|
||||
.distinct()
|
||||
.toList());
|
||||
}
|
||||
}));
|
||||
PLAYER_COLUMNS.put("locale", new OptionContainer(ParkourOption.LANG, (player, v) -> {
|
||||
player._locale = v;
|
||||
player.locale = v;
|
||||
}));
|
||||
PLAYER_COLUMNS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (player, v) -> player.schematicDifficulty = Double.parseDouble(v)));
|
||||
PLAYER_COLUMNS.put("sound", new OptionContainer(ParkourOption.SOUND, (player, v) -> player.sound = parseBoolean(v)));
|
||||
}
|
||||
public static final Map<String, OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
|
||||
|
||||
public @Expose Double schematicDifficulty;
|
||||
public @Expose Integer blockLead;
|
||||
|
|
@ -95,9 +69,7 @@ public class ParkourPlayer extends ParkourUser {
|
|||
}
|
||||
|
||||
private static boolean parseBoolean(String string) {
|
||||
return string == null
|
||||
|| string.equals("1") // for MySQL
|
||||
|| string.equals("true"); // for disk
|
||||
return string == null || string.equals("1") || string.equals("true");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -147,21 +119,7 @@ public class ParkourPlayer extends ParkourUser {
|
|||
* @param settings The settings map.
|
||||
*/
|
||||
public void setSettings(@NotNull Map<String, Object> settings) {
|
||||
for (String key : PLAYER_COLUMNS.keySet()) {
|
||||
Object value = settings.get(key);
|
||||
OptionContainer container = PLAYER_COLUMNS.get(key);
|
||||
|
||||
if (container.consumer == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
|
||||
container.consumer.accept(this, Option.OPTIONS_DEFAULTS.getOrDefault(container.option, ""));
|
||||
continue;
|
||||
}
|
||||
|
||||
container.consumer.accept(this, String.valueOf(value));
|
||||
}
|
||||
PlayerSettingsManager.applySettings(this, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -227,7 +185,6 @@ public class ParkourPlayer extends ParkourUser {
|
|||
}
|
||||
|
||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||
|
||||
}
|
||||
|
||||
private List<Integer> getEvenlyDistributedSlots(int count) {
|
||||
|
|
|
|||
|
|
@ -1,43 +1,25 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.event.ParkourJoinEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourLeaveEvent;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.hook.FloodgateHook;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.data.PreviousData;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import me.clip.placeholderapi.PlaceholderAPI;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
|
||||
import org.bukkit.scoreboard.DisplaySlot;
|
||||
import org.bukkit.scoreboard.Objective;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.bukkit.scoreboard.Team;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Superclass of every type of player. This encompasses every player currently in the Parkour world.
|
||||
|
|
@ -48,100 +30,31 @@ import java.util.stream.Collectors;
|
|||
public abstract class ParkourUser {
|
||||
|
||||
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
|
||||
PreviousData data = null;
|
||||
ParkourUser existing = getUser(player);
|
||||
|
||||
if (existing != null) {
|
||||
data = existing.previousData;
|
||||
unregister(existing, false, false, false);
|
||||
} else {
|
||||
}
|
||||
ParkourPlayer pp = new ParkourPlayer(player, session, data);
|
||||
|
||||
joinCount++;
|
||||
new ParkourJoinEvent(pp).call();
|
||||
|
||||
Storage.readPlayer(pp);
|
||||
return pp;
|
||||
return UserRegistry.register(player, session);
|
||||
}
|
||||
|
||||
public static void leave(@NotNull Player player) {
|
||||
ParkourUser user = getUser(player);
|
||||
if (user == null) return;
|
||||
leave(user);
|
||||
UserRegistry.leave(player);
|
||||
}
|
||||
|
||||
public static void leave(@NotNull ParkourUser user) {
|
||||
unregister(user, true, true, false);
|
||||
UserRegistry.leave(user);
|
||||
}
|
||||
|
||||
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
|
||||
new ParkourLeaveEvent(user).call();
|
||||
|
||||
try {
|
||||
user.unregister();
|
||||
|
||||
// Reset scoreboard to main
|
||||
if (user.board != null) {
|
||||
user.player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||
user.board = null;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
||||
"Error while trying to make player " + user.getName() + " leave", ex);
|
||||
user.send("<red><bold>There was an error while trying to handle leaving.");
|
||||
}
|
||||
|
||||
if (restorePreviousData && Config.CONFIG.getBoolean("bungeecord.enabled") && kickIfBungee) {
|
||||
sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!restorePreviousData) return;
|
||||
|
||||
user.previousData.apply(user.player, urgent);
|
||||
|
||||
Mode mode = user.session.generator.getMode();
|
||||
if (mode == null) {
|
||||
LoParkour.getPlugin().getLogger().severe("Mode is null for %s".formatted(user.getName()));
|
||||
mode = Modes.DEFAULT;
|
||||
}
|
||||
|
||||
if (user instanceof ParkourPlayer player) {
|
||||
Mode finalMode = mode;
|
||||
user.previousData.onLeave.forEach(r -> r.execute(player, finalMode));
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendPlayerToServer(Player player, String server) {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Connect");
|
||||
out.writeUTF(server);
|
||||
|
||||
try {
|
||||
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
|
||||
} catch (ChannelNotRegisteredException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error while trying to send %s to server %s. - %s".formatted(player.getName(), server, ex.getMessage()));
|
||||
player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server));
|
||||
}
|
||||
UserRegistry.unregister(user, restorePreviousData, kickIfBungee, urgent);
|
||||
}
|
||||
|
||||
public static boolean isUser(@Nullable Player player) {
|
||||
return player != null && getUsers().stream().anyMatch(other -> other.player == player);
|
||||
return UserRegistry.isUser(player);
|
||||
}
|
||||
|
||||
public static @Nullable ParkourUser getUser(@NotNull Player player) {
|
||||
return getUsers().stream()
|
||||
.filter(other -> other.getUUID().equals(player.getUniqueId()))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
return UserRegistry.getUser(player);
|
||||
}
|
||||
|
||||
public static Set<ParkourUser> getUsers() {
|
||||
return Divider.sections.keySet().stream()
|
||||
.flatMap(session -> session.getUsers().stream())
|
||||
.collect(Collectors.toSet());
|
||||
return UserRegistry.getUsers();
|
||||
}
|
||||
|
||||
// ─── Fields ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -153,7 +66,7 @@ public abstract class ParkourUser {
|
|||
public final Session session;
|
||||
public final Player player;
|
||||
public final Instant joined;
|
||||
public static int joinCount;
|
||||
private ScoreboardManager scoreboardManager;
|
||||
|
||||
public ParkourUser(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
|
||||
this.player = player;
|
||||
|
|
@ -163,7 +76,8 @@ public abstract class ParkourUser {
|
|||
|
||||
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
|
||||
this.board = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||
// Create the sidebar objective immediately and assign to player
|
||||
this.scoreboardManager = new ScoreboardManager(player, player.getUniqueId(), board, locale);
|
||||
|
||||
Objective obj = this.board.registerNewObjective("lp_sidebar", "dummy",
|
||||
ColorUtil.color(Locales.getString(locale, "scoreboard.title")));
|
||||
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||
|
|
@ -187,68 +101,11 @@ public abstract class ParkourUser {
|
|||
|
||||
/**
|
||||
* Updates the sidebar scoreboard for the given generator state.
|
||||
* Uses vanilla Bukkit scoreboard API — Team prefix trick for coloured lines.
|
||||
*/
|
||||
public void updateScoreboard(ParkourGenerator generator) {
|
||||
if (board == null || !generator.profile.get("showScoreboard").asBoolean()) {
|
||||
return;
|
||||
if (scoreboardManager != null) {
|
||||
scoreboardManager.update(generator);
|
||||
}
|
||||
|
||||
Leaderboard leaderboard = generator.getMode().getLeaderboard();
|
||||
Score top = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.getScoreAtRank(1);
|
||||
Score high = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.get(getUUID());
|
||||
if (top == null) top = new Score("?", "?", "?", 0);
|
||||
|
||||
// Update title
|
||||
Objective obj = board.getObjective("lp_sidebar");
|
||||
if (obj == null) {
|
||||
obj = board.registerNewObjective("lp_sidebar", "dummy",
|
||||
ColorUtil.color(replace(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
||||
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||
} else {
|
||||
obj.setDisplayName(ColorUtil.color(
|
||||
replace(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
||||
}
|
||||
|
||||
// Update lines using Team prefix trick (each line = unique entry).
|
||||
// We need globally-unique entries: use a combination of colour codes that gives
|
||||
// up to 32 unique strings (§0§0, §0§1, … §1§0 …). This safely covers any
|
||||
// reasonable scoreboard length.
|
||||
List<String> lines = replace(Locales.getStringList(locale, "scoreboard.lines"), top, high, generator);
|
||||
String[] codes = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String entry = "§" + codes[i / codes.length % codes.length] + "§" + codes[i % codes.length];
|
||||
String teamName = "lp_line_" + i;
|
||||
|
||||
Team team = board.getTeam(teamName);
|
||||
if (team == null) {
|
||||
team = board.registerNewTeam(teamName);
|
||||
team.addEntry(entry);
|
||||
}
|
||||
|
||||
team.setPrefix(ColorUtil.color(lines.get(i)));
|
||||
obj.getScore(entry).setScore(lines.size() - i);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> replace(List<String> s, Score top, Score high, ParkourGenerator generator) {
|
||||
return s.stream().map(line -> replace(line, top, high, generator)).toList();
|
||||
}
|
||||
|
||||
private String replace(String s, Score top, Score high, ParkourGenerator generator) {
|
||||
return ColorUtil.color(translate(player, s)
|
||||
.replace("%score%", Integer.toString(generator.score))
|
||||
.replace("%time%", generator.getFormattedTime())
|
||||
.replace("%difficulty%", Double.toString(generator.getDifficultyScore()))
|
||||
.replace("%top_score%", Integer.toString(top.score()))
|
||||
.replace("%top_player%", top.name())
|
||||
.replace("%top_time%", top.time())
|
||||
.replace("%high_score%", Integer.toString(high.score()))
|
||||
.replace("%high_score_time%", high.time()));
|
||||
}
|
||||
|
||||
private String translate(Player player, String string) {
|
||||
return LoParkour.getPlaceholderHook() == null ? string : PlaceholderAPI.setPlaceholders(player, string);
|
||||
}
|
||||
|
||||
public UUID getUUID() { return player.getUniqueId(); }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Manages player settings and their mapping to database columns.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class PlayerSettingsManager {
|
||||
|
||||
private static final Map<String, OptionContainer> COLUMN_MAPPINGS = new HashMap<>();
|
||||
|
||||
static {
|
||||
initializeColumnMappings();
|
||||
}
|
||||
|
||||
private static void initializeColumnMappings() {
|
||||
COLUMN_MAPPINGS.put("uuid", new OptionContainer(null, null));
|
||||
COLUMN_MAPPINGS.put("style", new OptionContainer(ParkourOption.STYLES, (p, v) -> p.style = v));
|
||||
COLUMN_MAPPINGS.put("blockLead", new OptionContainer(ParkourOption.LEADS, (p, v) -> p.blockLead = Integer.parseInt(v)));
|
||||
COLUMN_MAPPINGS.put("useParticles", new OptionContainer(ParkourOption.PARTICLES, (p, v) -> p.particles = parseBoolean(v)));
|
||||
COLUMN_MAPPINGS.put("useSpecial", new OptionContainer(ParkourOption.SPECIAL_BLOCKS, (p, v) -> p.useSpecialBlocks = parseBoolean(v)));
|
||||
COLUMN_MAPPINGS.put("showFallMsg", new OptionContainer(ParkourOption.FALL_MESSAGE, (p, v) -> p.showFallMessage = parseBoolean(v)));
|
||||
COLUMN_MAPPINGS.put("showScoreboard", new OptionContainer(ParkourOption.SCOREBOARD, (p, v) -> p.showScoreboard = parseBoolean(v)));
|
||||
COLUMN_MAPPINGS.put("selectedTime", new OptionContainer(ParkourOption.TIME, (p, v) -> p.selectedTime = Integer.parseInt(v)));
|
||||
COLUMN_MAPPINGS.put("collectedRewards", new OptionContainer(null, PlayerSettingsManager::applyCollectedRewards));
|
||||
COLUMN_MAPPINGS.put("locale", new OptionContainer(ParkourOption.LANG, (p, v) -> {
|
||||
p._locale = v;
|
||||
p.locale = v;
|
||||
}));
|
||||
COLUMN_MAPPINGS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (p, v) -> p.schematicDifficulty = Double.parseDouble(v)));
|
||||
COLUMN_MAPPINGS.put("sound", new OptionContainer(ParkourOption.SOUND, (p, v) -> p.sound = parseBoolean(v)));
|
||||
}
|
||||
|
||||
public static Map<String, OptionContainer> getColumnMappings() {
|
||||
return COLUMN_MAPPINGS;
|
||||
}
|
||||
|
||||
public static void applySettings(ParkourPlayer player, Map<String, Object> settings) {
|
||||
for (String key : COLUMN_MAPPINGS.keySet()) {
|
||||
Object value = settings.get(key);
|
||||
OptionContainer container = COLUMN_MAPPINGS.get(key);
|
||||
|
||||
if (container.consumer == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
|
||||
String defaultValue = Option.OPTIONS_DEFAULTS.getOrDefault(container.option, "");
|
||||
container.consumer.accept(player, defaultValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
container.consumer.accept(player, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean parseBoolean(String string) {
|
||||
return string == null || string.equals("1") || string.equals("true");
|
||||
}
|
||||
|
||||
private static void applyCollectedRewards(ParkourPlayer player, String value) {
|
||||
player.collectedRewards = new ArrayList<>();
|
||||
|
||||
if (!value.isEmpty()) {
|
||||
player.collectedRewards.addAll(Arrays.stream(value.replaceAll("[ \\[\\]]", "").split(","))
|
||||
.distinct()
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||
}
|
||||
}
|
||||
132
src/main/java/dev/loki/loparkour/player/ScoreboardManager.java
Normal file
132
src/main/java/dev/loki/loparkour/player/ScoreboardManager.java
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import me.clip.placeholderapi.PlaceholderAPI;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.DisplaySlot;
|
||||
import org.bukkit.scoreboard.Objective;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.bukkit.scoreboard.Team;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages scoreboard display for parkour users.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class ScoreboardManager {
|
||||
|
||||
private static final String[] COLOR_CODES = {
|
||||
"0", "1", "2", "3", "4", "5", "6", "7",
|
||||
"8", "9", "a", "b", "c", "d", "e", "f"
|
||||
};
|
||||
|
||||
private final Player player;
|
||||
private final UUID playerUUID;
|
||||
private final Scoreboard board;
|
||||
private final String locale;
|
||||
|
||||
public ScoreboardManager(Player player, UUID playerUUID, Scoreboard board, String locale) {
|
||||
this.player = player;
|
||||
this.playerUUID = playerUUID;
|
||||
this.board = board;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public void update(ParkourGenerator generator) {
|
||||
if (board == null || !generator.profile.get("showScoreboard").asBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Leaderboard leaderboard = generator.getMode().getLeaderboard();
|
||||
Score top = getTopScore(leaderboard);
|
||||
Score high = getHighScore(leaderboard);
|
||||
|
||||
updateTitle(top, high, generator);
|
||||
updateLines(top, high, generator);
|
||||
}
|
||||
|
||||
private Score getTopScore(Leaderboard leaderboard) {
|
||||
if (leaderboard == null) return createDefaultScore();
|
||||
Score score = leaderboard.getScoreAtRank(1);
|
||||
return score != null ? score : createDefaultScore();
|
||||
}
|
||||
|
||||
private Score getHighScore(Leaderboard leaderboard) {
|
||||
if (leaderboard == null) return createDefaultScore();
|
||||
Score score = leaderboard.get(playerUUID);
|
||||
return score != null ? score : createDefaultScore();
|
||||
}
|
||||
|
||||
private Score createDefaultScore() {
|
||||
return new Score("?", "?", "?", 0);
|
||||
}
|
||||
|
||||
private void updateTitle(Score top, Score high, ParkourGenerator generator) {
|
||||
Objective obj = board.getObjective("lp_sidebar");
|
||||
if (obj == null) {
|
||||
obj = board.registerNewObjective("lp_sidebar", "dummy",
|
||||
ColorUtil.color(replacePlaceholders(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
||||
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||
} else {
|
||||
obj.setDisplayName(ColorUtil.color(
|
||||
replacePlaceholders(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLines(Score top, Score high, ParkourGenerator generator) {
|
||||
Objective obj = board.getObjective("lp_sidebar");
|
||||
if (obj == null) return;
|
||||
|
||||
List<String> lines = Locales.getStringList(locale, "scoreboard.lines").stream()
|
||||
.map(line -> replacePlaceholders(line, top, high, generator))
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String entry = createUniqueEntry(i);
|
||||
String teamName = "lp_line_" + i;
|
||||
|
||||
Team team = board.getTeam(teamName);
|
||||
if (team == null) {
|
||||
team = board.registerNewTeam(teamName);
|
||||
team.addEntry(entry);
|
||||
}
|
||||
|
||||
team.setPrefix(ColorUtil.color(lines.get(i)));
|
||||
obj.getScore(entry).setScore(lines.size() - i);
|
||||
}
|
||||
}
|
||||
|
||||
private String createUniqueEntry(int index) {
|
||||
return "§" + COLOR_CODES[index / COLOR_CODES.length % COLOR_CODES.length] +
|
||||
"§" + COLOR_CODES[index % COLOR_CODES.length];
|
||||
}
|
||||
|
||||
private String replacePlaceholders(String text, Score top, Score high, ParkourGenerator generator) {
|
||||
String replaced = text
|
||||
.replace("%score%", Integer.toString(generator.state.score))
|
||||
.replace("%time%", generator.getFormattedTime())
|
||||
.replace("%difficulty%", Double.toString(generator.getDifficultyScore()))
|
||||
.replace("%top_score%", Integer.toString(top.score()))
|
||||
.replace("%top_player%", top.name())
|
||||
.replace("%top_time%", top.time())
|
||||
.replace("%high_score%", Integer.toString(high.score()))
|
||||
.replace("%high_score_time%", high.time());
|
||||
|
||||
return applyPlaceholderAPI(replaced);
|
||||
}
|
||||
|
||||
private String applyPlaceholderAPI(String text) {
|
||||
try {
|
||||
return PlaceholderAPI.setPlaceholders(player, text);
|
||||
} catch (Exception e) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/main/java/dev/loki/loparkour/player/UserRegistry.java
Normal file
128
src/main/java/dev/loki/loparkour/player/UserRegistry.java
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package dev.loki.loparkour.player;
|
||||
|
||||
import dev.loki.loparkour.api.event.ParkourJoinEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourLeaveEvent;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.data.PreviousData;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Manages registration and unregistration of parkour users.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class UserRegistry {
|
||||
|
||||
private static int joinCount = 0;
|
||||
|
||||
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
|
||||
PreviousData data = null;
|
||||
ParkourUser existing = getUser(player);
|
||||
|
||||
if (existing != null) {
|
||||
data = existing.previousData;
|
||||
unregister(existing, false, false, false);
|
||||
}
|
||||
|
||||
ParkourPlayer pp = new ParkourPlayer(player, session, data);
|
||||
joinCount++;
|
||||
new ParkourJoinEvent(pp).call();
|
||||
Storage.readPlayer(pp);
|
||||
|
||||
return pp;
|
||||
}
|
||||
|
||||
public static void leave(@NotNull Player player) {
|
||||
ParkourUser user = getUser(player);
|
||||
if (user != null) {
|
||||
leave(user);
|
||||
}
|
||||
}
|
||||
|
||||
public static void leave(@NotNull ParkourUser user) {
|
||||
unregister(user, true, true, false);
|
||||
}
|
||||
|
||||
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isUser(@Nullable Player player) {
|
||||
return player != null && getUsers().stream().anyMatch(other -> other.player == player);
|
||||
}
|
||||
|
||||
public static @Nullable ParkourUser getUser(@NotNull Player player) {
|
||||
return getUsers().stream()
|
||||
.filter(other -> other.getUUID().equals(player.getUniqueId()))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static Set<ParkourUser> getUsers() {
|
||||
return Divider.sections.keySet().stream()
|
||||
.flatMap(session -> session.getUsers().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static int getJoinCount() {
|
||||
return joinCount;
|
||||
}
|
||||
|
||||
private static void resetScoreboard(ParkourUser user) {
|
||||
if (user.board != null) {
|
||||
user.player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||
user.board = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleUnregisterError(ParkourUser user, Exception ex) {
|
||||
user.player.getServer().getLogger().log(java.util.logging.Level.SEVERE,
|
||||
"Error while trying to make player " + user.getName() + " leave", ex);
|
||||
user.send("<red><bold>There was an error while trying to handle leaving.");
|
||||
}
|
||||
|
||||
private static boolean shouldKickToBungee(boolean kickIfBungee) {
|
||||
return Config.CONFIG.getBoolean("bungeecord.enabled") && kickIfBungee;
|
||||
}
|
||||
|
||||
private static void restorePreviousState(ParkourUser user, boolean urgent) {
|
||||
user.previousData.apply(user.player, urgent);
|
||||
|
||||
Mode mode = user.session.generator.getMode();
|
||||
if (mode == null) {
|
||||
mode = Modes.DEFAULT;
|
||||
}
|
||||
|
||||
if (user instanceof ParkourPlayer player) {
|
||||
Mode finalMode = mode;
|
||||
user.previousData.onLeave.forEach(r -> r.execute(player, finalMode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,14 +42,9 @@ public class Session {
|
|||
private Visibility visibility = Visibility.PUBLIC;
|
||||
|
||||
/**
|
||||
* List of muted users.
|
||||
* Manages users (players and spectators) in this session.
|
||||
*/
|
||||
private final List<ParkourUser> muted = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* List of users.
|
||||
*/
|
||||
private final Map<UUID, ParkourUser> users = new HashMap<>();
|
||||
private final SessionUserManager userManager = new SessionUserManager(this);
|
||||
|
||||
/**
|
||||
* Function that takes the current session and returns whether new players should be accepted.
|
||||
|
|
@ -121,13 +116,7 @@ public class Session {
|
|||
* @param toAdd The players to add.
|
||||
*/
|
||||
public void addPlayers(ParkourPlayer... toAdd) {
|
||||
for (ParkourPlayer player : toAdd) {
|
||||
for (ParkourPlayer to : getPlayers()) {
|
||||
to.send(Locales.getString(player.locale, "lobby.other_join").formatted(player.getName()));
|
||||
}
|
||||
|
||||
users.put(player.getUUID(), player);
|
||||
}
|
||||
userManager.addPlayers(toAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,31 +125,14 @@ public class Session {
|
|||
* @param toRemove The players to remove.
|
||||
*/
|
||||
public void removePlayers(ParkourPlayer... toRemove) {
|
||||
for (ParkourPlayer player : toRemove) {
|
||||
users.remove(player.getUUID());
|
||||
}
|
||||
|
||||
List<ParkourPlayer> players = getPlayers();
|
||||
for (ParkourPlayer player : toRemove) {
|
||||
for (ParkourPlayer to : players) {
|
||||
to.send(Locales.getString(player.locale, "lobby.other_leave").formatted(player.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.length > 0 && players.isEmpty()) {
|
||||
generator.reset(false);
|
||||
Divider.remove(this);
|
||||
}
|
||||
userManager.removePlayers(toRemove);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The players.
|
||||
*/
|
||||
public List<ParkourPlayer> getPlayers() {
|
||||
return users.values().stream()
|
||||
.filter(user -> user instanceof ParkourPlayer)
|
||||
.map(user -> (ParkourPlayer) user)
|
||||
.toList();
|
||||
return userManager.getPlayers();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -169,13 +141,7 @@ public class Session {
|
|||
* @param spectators The spectators to add.
|
||||
*/
|
||||
public void addSpectators(ParkourSpectator... spectators) {
|
||||
for (ParkourSpectator spectator : spectators) {
|
||||
for (ParkourPlayer player : getPlayers()) {
|
||||
player.sendTranslated("play.spectator.other_join", spectator.getName());
|
||||
}
|
||||
|
||||
users.put(spectator.getUUID(), spectator);
|
||||
}
|
||||
userManager.addSpectators(spectators);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,30 +150,21 @@ public class Session {
|
|||
* @param spectators The spectators to remove.
|
||||
*/
|
||||
public void removeSpectators(ParkourSpectator... spectators) {
|
||||
for (ParkourSpectator spectator : spectators) {
|
||||
for (ParkourPlayer player : getPlayers()) {
|
||||
player.sendTranslated("play.spectator.other_leave", spectator.getName());
|
||||
}
|
||||
|
||||
users.remove(spectator.getUUID());
|
||||
}
|
||||
userManager.removeSpectators(spectators);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The spectators.
|
||||
*/
|
||||
public List<ParkourSpectator> getSpectators() {
|
||||
return users.values().stream()
|
||||
.filter(user -> user instanceof ParkourSpectator)
|
||||
.map(user -> (ParkourSpectator) user)
|
||||
.toList();
|
||||
return userManager.getSpectators();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The users.
|
||||
*/
|
||||
public List<ParkourUser> getUsers() {
|
||||
return new ArrayList<>(users.values());
|
||||
return userManager.getUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -224,9 +181,7 @@ public class Session {
|
|||
* @param user The user to (un)mute.
|
||||
*/
|
||||
public void toggleMute(@NotNull ParkourUser user) {
|
||||
if (!muted.remove(user)) {
|
||||
muted.add(user);
|
||||
}
|
||||
userManager.toggleMute(user);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -234,7 +189,15 @@ public class Session {
|
|||
* @return True when the user is muted, false if not.
|
||||
*/
|
||||
public boolean isMuted(@NotNull ParkourUser user) {
|
||||
return muted.contains(user);
|
||||
return userManager.isMuted(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when all players have left the session.
|
||||
*/
|
||||
void onAllPlayersLeft() {
|
||||
generator.reset(false);
|
||||
Divider.remove(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
112
src/main/java/dev/loki/loparkour/session/SessionUserManager.java
Normal file
112
src/main/java/dev/loki/loparkour/session/SessionUserManager.java
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package dev.loki.loparkour.session;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Manages users (players and spectators) within a session.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class SessionUserManager {
|
||||
|
||||
private final Map<UUID, ParkourUser> users = new HashMap<>();
|
||||
private final List<ParkourUser> muted = new ArrayList<>();
|
||||
private final Session session;
|
||||
|
||||
public SessionUserManager(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void addPlayers(ParkourPlayer... toAdd) {
|
||||
for (ParkourPlayer player : toAdd) {
|
||||
notifyJoin(player);
|
||||
users.put(player.getUUID(), player);
|
||||
}
|
||||
}
|
||||
|
||||
public void removePlayers(ParkourPlayer... toRemove) {
|
||||
for (ParkourPlayer player : toRemove) {
|
||||
users.remove(player.getUUID());
|
||||
}
|
||||
|
||||
List<ParkourPlayer> remainingPlayers = getPlayers();
|
||||
notifyLeave(toRemove, remainingPlayers);
|
||||
|
||||
if (toRemove.length > 0 && remainingPlayers.isEmpty()) {
|
||||
session.onAllPlayersLeft();
|
||||
}
|
||||
}
|
||||
|
||||
public void addSpectators(ParkourSpectator... spectators) {
|
||||
for (ParkourSpectator spectator : spectators) {
|
||||
notifySpectatorJoin(spectator);
|
||||
users.put(spectator.getUUID(), spectator);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSpectators(ParkourSpectator... spectators) {
|
||||
for (ParkourSpectator spectator : spectators) {
|
||||
notifySpectatorLeave(spectator);
|
||||
users.remove(spectator.getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
public List<ParkourPlayer> getPlayers() {
|
||||
return users.values().stream()
|
||||
.filter(user -> user instanceof ParkourPlayer)
|
||||
.map(user -> (ParkourPlayer) user)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<ParkourSpectator> getSpectators() {
|
||||
return users.values().stream()
|
||||
.filter(user -> user instanceof ParkourSpectator)
|
||||
.map(user -> (ParkourSpectator) user)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<ParkourUser> getUsers() {
|
||||
return new ArrayList<>(users.values());
|
||||
}
|
||||
|
||||
public void toggleMute(ParkourUser user) {
|
||||
if (!muted.remove(user)) {
|
||||
muted.add(user);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMuted(ParkourUser user) {
|
||||
return muted.contains(user);
|
||||
}
|
||||
|
||||
private void notifyJoin(ParkourPlayer player) {
|
||||
for (ParkourPlayer to : getPlayers()) {
|
||||
to.send(Locales.getString(player.locale, "lobby.other_join").formatted(player.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyLeave(ParkourPlayer[] toRemove, List<ParkourPlayer> remainingPlayers) {
|
||||
for (ParkourPlayer player : toRemove) {
|
||||
for (ParkourPlayer to : remainingPlayers) {
|
||||
to.send(Locales.getString(player.locale, "lobby.other_leave").formatted(player.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySpectatorJoin(ParkourSpectator spectator) {
|
||||
for (ParkourPlayer player : getPlayers()) {
|
||||
player.sendTranslated("play.spectator.other_join", spectator.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySpectatorLeave(ParkourSpectator spectator) {
|
||||
for (ParkourPlayer player : getPlayers()) {
|
||||
player.sendTranslated("play.spectator.other_leave", spectator.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Manages SQL database connection lifecycle.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class SQLConnectionManager {
|
||||
|
||||
private Connection connection;
|
||||
private volatile boolean connected = false;
|
||||
private final List<Runnable> onConnectCallbacks = new CopyOnWriteArrayList<>();
|
||||
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
public void runWhenConnected(Runnable callback) {
|
||||
if (connected) {
|
||||
callback.run();
|
||||
} else {
|
||||
onConnectCallbacks.add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
try {
|
||||
LoParkour.log("Connecting to MySQL...");
|
||||
loadDriver();
|
||||
|
||||
connection = DriverManager.getConnection(
|
||||
buildConnectionUrl(),
|
||||
Option.SQL_USERNAME,
|
||||
Option.SQL_PASSWORD);
|
||||
|
||||
connected = true;
|
||||
LoParkour.log("Connected to MySQL");
|
||||
|
||||
executeCallbacks();
|
||||
} catch (Exception ex) {
|
||||
handleConnectionError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
connection.close();
|
||||
LoParkour.log("Closed connection to MySQL");
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error while trying to close connection to SQL database - " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedStatement prepareStatement(String sql) {
|
||||
validateConnection();
|
||||
if (connection == null) return null;
|
||||
|
||||
try {
|
||||
return connection.prepareStatement(sql);
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error preparing statement: %s - %s".formatted(sql, ex.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void validateConnection() {
|
||||
try {
|
||||
if (connection == null || !connection.isValid(2)) {
|
||||
LoParkour.getPlugin().getLogger().warning("MySQL connection lost, attempting reconnect...");
|
||||
connect();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error reconnecting to MySQL - " + ex.getMessage());
|
||||
Option.SQL = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDriver() throws ClassNotFoundException {
|
||||
try {
|
||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildConnectionUrl() {
|
||||
return ("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
|
||||
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
|
||||
"&autoReconnect=true&maxReconnects=2&connectTimeout=5000&socketTimeout=5000")
|
||||
.formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB);
|
||||
}
|
||||
|
||||
private void executeCallbacks() {
|
||||
for (Runnable cb : onConnectCallbacks) {
|
||||
try {
|
||||
cb.run();
|
||||
} catch (Exception e) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
onConnectCallbacks.clear();
|
||||
}
|
||||
|
||||
private void handleConnectionError(Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Could not connect to MySQL - check your SQL settings - " + ex.getMessage());
|
||||
LoParkour.getPlugin().getLogger().severe("Disabling SQL storage, using local storage instead");
|
||||
Option.SQL = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
|
||||
/**
|
||||
* Manages database schema migrations.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class SQLMigrationManager {
|
||||
|
||||
private final SQLQueryExecutor queryExecutor;
|
||||
|
||||
public SQLMigrationManager(SQLQueryExecutor queryExecutor) {
|
||||
this.queryExecutor = queryExecutor;
|
||||
}
|
||||
|
||||
public void initializeDatabase() {
|
||||
createDatabase();
|
||||
createOptionsTable();
|
||||
runMigrations();
|
||||
}
|
||||
|
||||
private void createDatabase() {
|
||||
queryExecutor.executeUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
|
||||
queryExecutor.executeUpdate("USE `%s`;".formatted(Option.SQL_DB));
|
||||
}
|
||||
|
||||
private void createOptionsTable() {
|
||||
queryExecutor.executeUpdate(
|
||||
("CREATE TABLE IF NOT EXISTS `%soptions` " +
|
||||
"(`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, " +
|
||||
"`useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, " +
|
||||
"`showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) " +
|
||||
"ENGINE = InnoDB CHARSET = utf8;").formatted(Option.SQL_PREFIX));
|
||||
}
|
||||
|
||||
private void runMigrations() {
|
||||
String prefix = Option.SQL_PREFIX;
|
||||
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `collectedRewards` MEDIUMTEXT;".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `locale` VARCHAR(8);".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `schematicDifficulty` DOUBLE;".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` ADD `sound` BOOLEAN;".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(prefix));
|
||||
queryExecutor.executeUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(prefix));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Executes SQL queries with error handling.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
class SQLQueryExecutor {
|
||||
|
||||
private final SQLConnectionManager connectionManager;
|
||||
|
||||
public SQLQueryExecutor(SQLConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public void executeUpdate(String sql) {
|
||||
executeUpdate(sql, false);
|
||||
}
|
||||
|
||||
public void executeUpdateSuppressed(String sql) {
|
||||
executeUpdate(sql, true);
|
||||
}
|
||||
|
||||
private void executeUpdate(String sql, boolean suppressErrors) {
|
||||
connectionManager.validateConnection();
|
||||
|
||||
try (PreparedStatement stmt = connectionManager.prepareStatement(sql)) {
|
||||
if (stmt != null) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
if (!suppressErrors) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error while sending update: %s - %s".formatted(sql, ex.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedStatement prepareStatement(String sql) {
|
||||
return connectionManager.prepareStatement(sql);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,32 +21,19 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||
*/
|
||||
class StorageSQL {
|
||||
|
||||
private static Connection connection;
|
||||
private static boolean initialized = false;
|
||||
private static volatile boolean connected = false;
|
||||
private static final SQLConnectionManager connectionManager = new SQLConnectionManager();
|
||||
private static final SQLQueryExecutor queryExecutor = new SQLQueryExecutor(connectionManager);
|
||||
private static final SQLMigrationManager migrationManager = new SQLMigrationManager(queryExecutor);
|
||||
|
||||
// Tables queued to create after connection is established
|
||||
private static boolean initialized = false;
|
||||
private static final List<String> pendingTableCreations = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Callbacks to run after connection is ready (e.g. initial leaderboard reads)
|
||||
private static final List<Runnable> onConnectCallbacks = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Returns true if the SQL connection is ready.
|
||||
*/
|
||||
public static boolean isConnected() {
|
||||
return connected;
|
||||
return connectionManager.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given callback immediately if already connected, or defers it until connect() succeeds.
|
||||
*/
|
||||
public static void runWhenConnected(Runnable callback) {
|
||||
if (connected) {
|
||||
callback.run();
|
||||
} else {
|
||||
onConnectCallbacks.add(callback);
|
||||
}
|
||||
connectionManager.runWhenConnected(callback);
|
||||
}
|
||||
|
||||
public static void init(String mode) {
|
||||
|
|
@ -64,30 +51,20 @@ class StorageSQL {
|
|||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
// Queue this table for creation after connect() finishes
|
||||
pendingTableCreations.add(createTableSql);
|
||||
// Connect async — table creation happens inside connect() after connection is ready
|
||||
Bukkit.getScheduler().runTaskAsynchronously(LoParkour.getPlugin(), StorageSQL::connect);
|
||||
} else {
|
||||
// Already connected (or connecting) — send directly
|
||||
pendingTableCreations.add(createTableSql);
|
||||
}
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
try {
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
connection.close();
|
||||
LoParkour.log("Closed connection to MySQL");
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error while trying to close connection to SQL database - " + ex.getMessage());
|
||||
}
|
||||
connectionManager.close();
|
||||
}
|
||||
|
||||
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
||||
String sql = "SELECT * FROM `%s`;".formatted(getTableName(mode));
|
||||
try (PreparedStatement stmt = prepareStatement(sql)) {
|
||||
try (PreparedStatement stmt = queryExecutor.prepareStatement(sql)) {
|
||||
if (stmt == null) return new HashMap<>();
|
||||
try (ResultSet results = stmt.executeQuery()) {
|
||||
Map<UUID, Score> scores = new HashMap<>();
|
||||
|
|
@ -101,13 +78,14 @@ class StorageSQL {
|
|||
return scores;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error while trying to read SQL data of %s - %s".formatted(mode, ex.getMessage()));
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error while trying to read SQL data of %s - %s".formatted(mode, ex.getMessage()));
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||
new HashMap<>(scores).forEach((uuid, score) -> sendUpdate("""
|
||||
new HashMap<>(scores).forEach((uuid, score) -> queryExecutor.executeUpdate("""
|
||||
INSERT INTO `%s`
|
||||
(uuid, name, time, difficulty, score)
|
||||
VALUES ('%s', '%s', '%s', '%s', %d)
|
||||
|
|
@ -125,10 +103,16 @@ class StorageSQL {
|
|||
|
||||
public static void readPlayer(@NotNull ParkourPlayer player) {
|
||||
String sql = "SELECT * FROM `%soptions` WHERE uuid = '%s';".formatted(Option.SQL_PREFIX, player.getUUID());
|
||||
try (PreparedStatement stmt = prepareStatement(sql)) {
|
||||
if (stmt == null) { player.setSettings(new HashMap<>()); return; }
|
||||
try (PreparedStatement stmt = queryExecutor.prepareStatement(sql)) {
|
||||
if (stmt == null) {
|
||||
player.setSettings(new HashMap<>());
|
||||
return;
|
||||
}
|
||||
try (ResultSet results = stmt.executeQuery()) {
|
||||
if (!results.next()) { player.setSettings(new HashMap<>()); return; }
|
||||
if (!results.next()) {
|
||||
player.setSettings(new HashMap<>());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
for (String key : ParkourPlayer.PLAYER_COLUMNS.keySet()) {
|
||||
|
|
@ -142,7 +126,8 @@ class StorageSQL {
|
|||
player.setSettings(settings);
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error reading SQL data of %s - %s".formatted(player.getName(), ex.getMessage()));
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error reading SQL data of %s - %s".formatted(player.getName(), ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +135,7 @@ class StorageSQL {
|
|||
DecimalFormat df = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
String schematicDifficulty = df.format(player.schematicDifficulty);
|
||||
|
||||
sendUpdate("""
|
||||
queryExecutor.executeUpdate("""
|
||||
INSERT INTO `%soptions`
|
||||
(uuid, style, blockLead, useParticles, useSpecial, showFallMsg, showScoreboard,
|
||||
selectedTime, collectedRewards, locale, schematicDifficulty, sound)
|
||||
|
|
@ -178,105 +163,12 @@ class StorageSQL {
|
|||
}
|
||||
|
||||
private static void connect() {
|
||||
try {
|
||||
LoParkour.log("Connecting to MySQL...");
|
||||
connectionManager.connect();
|
||||
migrationManager.initializeDatabase();
|
||||
|
||||
try {
|
||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
}
|
||||
|
||||
connection = DriverManager.getConnection(
|
||||
("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
|
||||
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
|
||||
"&autoReconnect=true&maxReconnects=2&connectTimeout=5000&socketTimeout=5000")
|
||||
.formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB),
|
||||
Option.SQL_USERNAME, Option.SQL_PASSWORD);
|
||||
|
||||
sendUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
|
||||
sendUpdate("USE `%s`;".formatted(Option.SQL_DB));
|
||||
|
||||
// Base options table
|
||||
sendUpdate(("CREATE TABLE IF NOT EXISTS `%soptions` " +
|
||||
"(`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, " +
|
||||
"`useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, " +
|
||||
"`showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) " +
|
||||
"ENGINE = InnoDB CHARSET = utf8;").formatted(Option.SQL_PREFIX));
|
||||
|
||||
// Migrations
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `collectedRewards` MEDIUMTEXT;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `locale` VARCHAR(8);".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `schematicDifficulty` DOUBLE;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `sound` BOOLEAN;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(Option.SQL_PREFIX));
|
||||
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(Option.SQL_PREFIX));
|
||||
|
||||
// Now flush all pending table creations (leaderboard tables queued before connection was ready)
|
||||
for (String sql : pendingTableCreations) {
|
||||
sendUpdate(sql);
|
||||
}
|
||||
pendingTableCreations.clear();
|
||||
|
||||
LoParkour.log("Connected to MySQL");
|
||||
connected = true;
|
||||
|
||||
// Run deferred callbacks (e.g. initial leaderboard reads)
|
||||
for (Runnable cb : onConnectCallbacks) {
|
||||
try { cb.run(); } catch (Exception e) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
onConnectCallbacks.clear();
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Could not connect to MySQL - check your SQL settings - " + ex.getMessage());
|
||||
LoParkour.getPlugin().getLogger().severe("Disabling SQL storage, using local storage instead");
|
||||
Option.SQL = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateConnection() {
|
||||
try {
|
||||
if (connection == null || !connection.isValid(2)) {
|
||||
LoParkour.getPlugin().getLogger().warning("MySQL connection lost, attempting reconnect...");
|
||||
connect();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error reconnecting to MySQL - " + ex.getMessage());
|
||||
Option.SQL = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a PreparedStatement with proper connection validation. Caller must close it. */
|
||||
private static PreparedStatement prepareStatement(String sql) {
|
||||
validateConnection();
|
||||
if (connection == null) return null;
|
||||
try {
|
||||
return connection.prepareStatement(sql);
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error preparing statement: %s - %s".formatted(sql, ex.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendUpdate(String sql) {
|
||||
validateConnection();
|
||||
if (connection == null) return;
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error while sending update: %s - %s".formatted(sql, ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendUpdateSuppressed(String sql) {
|
||||
validateConnection();
|
||||
if (connection == null) return;
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException ignored) {
|
||||
for (String sql : pendingTableCreations) {
|
||||
queryExecutor.executeUpdate(sql);
|
||||
}
|
||||
pendingTableCreations.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue