Folder refactoring fixed the broken file

This commit is contained in:
loki 2026-03-07 12:58:07 +01:00
parent 574f9fc4b5
commit 261c4a5cab
24 changed files with 385 additions and 150 deletions

View file

@ -1,91 +0,0 @@
# Рефакторинг - Краткое описание
## Выполненные изменения
### 1. StorageSQL.java (248 → ~150 строк)
**Проблемы:**
- Смешивание логики подключения, миграций и CRUD операций
- Дублирование кода в методах sendUpdate/sendUpdateSuppressed
- Отсутствие разделения ответственностей
**Решение:**
Разделен на 3 класса:
- `SQLConnectionManager` - управление подключением к БД
- `SQLQueryExecutor` - выполнение SQL запросов
- `SQLMigrationManager` - управление миграциями схемы БД
**Преимущества:**
- Каждый класс отвечает за одну задачу (Single Responsibility Principle)
- Легче тестировать и поддерживать
- Упрощена логика переподключения
### 2. Session.java (236 → ~120 строк)
**Проблемы:**
- Слишком много ответственностей (игроки, спектаторы, видимость, мут)
- Дублирование логики в add/remove методах
- Сложная логика уведомлений
**Решение:**
Создан класс `SessionUserManager` для управления пользователями
**Преимущества:**
- Session теперь делегирует управление пользователями
- Логика уведомлений инкапсулирована
- Проще добавлять новые типы пользователей
### 3. ParkourUser.java (220 → ~100 строк)
**Проблемы:**
- Смешивание статических методов регистрации и instance методов
- Сложная логика scoreboard встроена в класс
- Запутанные методы register/unregister/leave
**Решение:**
Разделен на 3 класса:
- `UserRegistry` - регистрация и управление пользователями
- `ScoreboardManager` - управление scoreboard
- `BungeeUtil` - утилиты для BungeeCord
**Преимущества:**
- Четкое разделение статической и instance логики
- Scoreboard логика изолирована и переиспользуема
- Упрощена логика регистрации/выхода
### 4. ParkourPlayer.java (202 → ~150 строк)
**Проблемы:**
- Огромная статическая инициализация PLAYER_COLUMNS
- Дублирование логики с ParkourUser
- Сложный метод setSettings
**Решение:**
Создан класс `PlayerSettingsManager` для управления настройками
**Преимущества:**
- Настройки и их маппинг инкапсулированы
- Легче добавлять новые настройки
- Упрощена логика применения настроек
## Итоговая статистика
| Файл | Было строк | Стало строк | Новых классов |
|------|------------|-------------|---------------|
| StorageSQL.java | 248 | ~150 | 3 |
| Session.java | 236 | ~120 | 1 |
| ParkourUser.java | 220 | ~100 | 3 |
| ParkourPlayer.java | 202 | ~150 | 1 |
| **ИТОГО** | **906** | **~520** | **8** |
## Принципы, примененные в рефакторинге
1. **Single Responsibility Principle (SRP)** - каждый класс отвечает за одну задачу
2. **Separation of Concerns** - разделение логики по разным классам
3. **DRY (Don't Repeat Yourself)** - устранение дублирования кода
4. **Encapsulation** - инкапсуляция сложной логики в отдельные классы
5. **Delegation** - делегирование задач специализированным классам
## Обратная совместимость
Все публичные API остались без изменений. Рефакторинг затронул только внутреннюю структуру классов.

64
TODO.md
View file

@ -14,6 +14,12 @@
| # | Баг | Файл | Что сделать |
|---|-----|------|-------------|
| ✅ | ~~BOM в `ParkourGenerator.java` ломает компиляцию~~ | `ParkourGenerator.java` | ✅ Удалён через Python |
| ✅ | ~~`generator.score` → `generator.state.score`~~ | `PAPIHook.java`, `ParkourSettingsMenu.java` | ✅ Исправлено |
| ✅ | ~~`ParkourUser.joinCount` удалён~~ | `LoParkour.java` | ✅ Использован `UserRegistry.getJoinCount()` |
| ✅ | ~~`PlayerSettingsManager.OptionContainer` не public~~ | `PlayerSettingsManager.java` | ✅ Сделан public |
| ✅ | ~~`Option.PARTICLE_DATA.data()` не существует~~ | `EffectManager.java` | ✅ Убран `.data()` |
| ✅ | ~~`ParticleUtil.circle()` неправильные параметры~~ | `EffectManager.java` | ✅ Исправлено на (Location, Particle, int, int) |
| 1 | `PotionEffectType.JUMP` и `SLOW` удалены в Paper 1.20.5+ | `GravityShiftMode.java` | Заменить на `JUMP_BOOST` и `SLOWNESS` |
| 2 | `ElytraMode` создаёт обычный `ParkourGenerator` → `getMode()` возвращает DEFAULT | `ElytraMode.java` | Создать `ElytraGenerator extends ParkourGenerator` |
| 3 | `SpeedrunMode`: таймеры `runLater` продолжают тикать после `reset()` — гонка с генератором | `SpeedrunMode.java` | Хранить `ScheduledTask`, отменять в `reset()` |
@ -27,21 +33,72 @@
| 4 | `HardcoreMode`: `collectedRewards` не очищается при падении | `HardcoreMode.java` | Добавить `player.collectedRewards.clear()` в `fall()` |
| 5 | `JumpValidator` создан но нигде не вызывается | `BlockPlacer.java` | Добавить retry-loop в `selectNext()` (до 10 попыток) |
| 6 | `JumpType` enum создан но не интегрирован в генератор | `BlockPlacer.java` | Добавить выбор JumpType по шансу из конфига |
| 7 | `ConfigUpdater` закомментирован | `Config.java` | Раскомментировать, проверить `ignoredSections` |
| 7 | ~~`ConfigUpdater` закомментирован~~ | `Config.java` | ✅ Написан `ConfigUpdater.java`, подключён в `Config.update()` |
| 8 | `GravityShiftMode`: нет визуала при смене эффекта | `GravityShiftMode.java` | Добавить Title + Sound + партиклы `PORTAL` |
---
## 🟢 Планово — дуэли
### DuelMode
- [ ] `DuelMode implements Mode` — режим дуэли
- [ ] `DuelSession extends Session` — два игрока, отдельные острова
- [ ] `/lp duel <игрок>` — вызов на дуэль, запрос истекает через 30 сек
- [ ] Форматы: **Race** (кто первый до N очков), **Survival** (3 жизни), **Time Attack** (60 сек кто больше)
- [ ] Scoreboard показывает счёт обоих в реальном времени
- [ ] ActionBar: `Opponent: 34 pts` во время игры
- [ ] При падении противника — уведомление
- [ ] Победитель получает награды через систему `Rewards`
- [ ] Лидерборд дуэлей (W/L)
- [ ] Кнопка реванша после окончания
---
## 🟢 Планово
| # | Фича | Файл | Что сделать |
|---|------|------|-------------|
| 9 | Ghost система не интегрирована | `ghost/` | Подключить `GhostRecorder` к `GeneratorLifecycle` |
| 10 | Удалить мёртвые stub-файлы | `generator/`, корень | `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
| ✅ | ~~Удалить мёртвые stub-файлы~~ | `generator/`, корень | ✅ Удалены: `Command.java`, `Events.java`, `BlockSelector.java`, `ScoreManager.java`, `GeneratorTick.java` |
| 11 | ElytraMode: кольца из партиклов + детекция пролёта | `ElytraMode.java` | BoundingBox check в `tick()` |
| 12 | ElytraMode: cooldown на фейерверки | `ElytraMode.java` | Счётчик + сообщение |
| 13 | RaceMode | новый файл | Кто первый наберёт N очков |
| 14 | CoopMode | новый файл | Общий счёт нескольких игроков |
| ✅ | ~~Реорганизация `generator/` по подпакетам~~ | `generator/` | ✅ Создана структура: `jump/`, `lifecycle/`, `effect/` |
---
## 🏗️ ✅ Реорганизация generator/ — ГОТОВО
Новая структура по логическим группам:
```
generator/
├── ParkourGenerator.java ← главный класс (183 строки)
├── GeneratorState.java ← данные генератора
├── GeneratorOption.java ← enum опций
├── Profile.java ← профиль настроек
├── Island.java ← остров спавна
│
├── jump/ ← всё про прыжки и размещение блоков
│ ├── BlockPlacer.java (166 строк)
│ ├── JumpDirector.java
│ ├── JumpOffsetGenerator.java
│ ├── JumpValidator.java
│ └── JumpType.java
│
├── lifecycle/ ← жизненный цикл: тик, падение, очистка
│ └── GeneratorLifecycle.java
│
└── effect/ ← визуальные эффекты
└── EffectManager.java
```
**Результат:**
- ✅ Все файлы перемещены через `smartRelocate` (импорты обновлены автоматически)
- ✅ Удалены мёртвые stub-файлы
- ✅ Проект компилируется без ошибок
- ✅ Структура стала логичнее и понятнее
---
@ -55,12 +112,13 @@
- `LoParkourCommand` разбит на `PlayerCommandHandler` + `AdminCommandHandler` + `SchematicCommandHandler`
- `Events` разбит на 3 listener'а
- `StorageSQL` разбит на `SQLConnectionManager` + `SQLQueryExecutor` + `SQLMigrationManager`
- ✅ **Проект компилируется без ошибок** (исправлены все баги после рефакторинга)
---
## 🎮 Чек-лист перед релизом
- [ ] `./gradlew build` без ошибок
- [x] `./gradlew build` без ошибок
- [ ] Запуск сервера без ошибок в консоли
- [ ] Паркур: прыжки, счёт, рекорд работают
- [ ] Scoreboard обновляется

View file

@ -1,7 +0,0 @@
package dev.loki.loparkour;
/**
* @deprecated Replaced by {@link LoParkourCommand}. Delete this file.
*/
@Deprecated
class Command extends LoParkourCommand {}

View file

@ -1,12 +0,0 @@
package dev.loki.loparkour;
/**
* @deprecated Split into:
* - {@link dev.loki.loparkour.listener.PlayerConnectionListener}
* - {@link dev.loki.loparkour.listener.ParkourRestrictionListener}
* - {@link dev.loki.loparkour.listener.SchematicWandListener}
*
* Delete this file after verifying compilation.
*/
@Deprecated
class Events {}

View file

@ -14,6 +14,7 @@ import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.mode.SpectatorMode;
import dev.loki.loparkour.mode.SpeedrunMode;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.player.UserRegistry;
import dev.loki.loparkour.reward.Rewards;
import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
import dev.loki.loparkour.storage.Storage;
@ -243,8 +244,7 @@ public final class LoParkour extends LoPlugin {
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.locales.size())));
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
int joins = ParkourUser.joinCount;
ParkourUser.joinCount = 0;
int joins = UserRegistry.getJoinCount();
return joins;
}));
}

View file

@ -87,10 +87,12 @@ public enum Config {
/**
* Updates the file so all keys are present.
* Any key that exists in the bundled default but is missing on disk
* will be appended to the correct section automatically.
*/
public void update() {
try {
// TODO: ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
} catch (Exception ex) {
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
"Error while trying to update config file: " + fileName, ex);

View file

@ -0,0 +1,287 @@
package dev.loki.loparkour.config;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.logging.Level;
/**
* Automatically adds missing keys from the default (bundled) config
* into the player's config file on disk, preserving all existing values,
* comments, and blank lines.
*
* <p>Algorithm:
* <ol>
* <li>Load the default config from the jar as a list of raw lines.</li>
* <li>Load the on-disk config as a flat key→value map.</li>
* <li>Walk the default lines; for every key that is missing on disk,
* emit the default line (with its comment header) into the output.</li>
* <li>Rewrite the on-disk file with the merged result.</li>
* </ol>
*
* Sections listed in {@code ignoredSections} are never touched — their
* default content is never injected (user manages them freely).
*/
public final class ConfigUpdater {
private ConfigUpdater() {}
/**
* Updates {@code diskFile} by injecting any keys that exist in the
* bundled default but are absent on disk.
*
* @param plugin the plugin (used to read the bundled resource)
* @param resourceName path inside the jar, e.g. {@code "config.yml"}
* @param diskFile the on-disk file to update
* @param ignoredSections top-level sections whose keys should never be added
* (pass {@code null} or empty list to ignore nothing)
*/
public static void update(
@NotNull Plugin plugin,
@NotNull String resourceName,
@NotNull File diskFile,
@Nullable List<String> ignoredSections
) throws IOException {
List<String> defaultLines = readResource(plugin, resourceName);
if (defaultLines == null) {
plugin.getLogger().warning("[ConfigUpdater] Resource not found in jar: " + resourceName);
return;
}
// Flat key set of what already exists on disk (dot-separated paths)
Set<String> diskKeys = flatKeys(diskFile);
// Ignored section prefixes (e.g. "styles" → skip any path starting with "styles.")
Set<String> ignored = normalizedIgnored(ignoredSections);
List<String> diskLines = readLines(diskFile);
List<String> outputLines = merge(defaultLines, diskLines, diskKeys, ignored);
// Only write if something actually changed
if (!outputLines.equals(diskLines)) {
writeLines(diskFile, outputLines);
plugin.getLogger().info("[ConfigUpdater] Updated " + resourceName
+ " — added " + countNew(diskLines, outputLines) + " new key(s).");
}
}
// ── Merge ─────────────────────────────────────────────────────────────────
/**
* Merges default lines into disk lines.
*
* <p>Strategy: walk disk lines first, emit them as-is. Then walk default
* lines; for each key block (comment + key line) whose key is absent on
* disk and not ignored, append it at the correct indentation level.
*/
private static List<String> merge(
List<String> defaultLines,
List<String> diskLines,
Set<String> diskKeys,
Set<String> ignored
) {
// We need to insert missing keys at the right position.
// Build a copy of disk lines we will extend.
List<String> result = new ArrayList<>(diskLines);
// Collect blocks from the default file: each block = list of comment
// lines + the key line itself. We track the "path stack" to know the
// full dotted path of every key.
Deque<String> pathStack = new ArrayDeque<>();
List<String> pendingComments = new ArrayList<>();
for (int i = 0; i < defaultLines.size(); i++) {
String raw = defaultLines.get(i);
String trimmed = raw.stripLeading();
// Blank line → flush pending comments, keep as separator
if (trimmed.isBlank()) {
pendingComments.add(raw);
continue;
}
// Comment line → accumulate
if (trimmed.startsWith("#")) {
pendingComments.add(raw);
continue;
}
// Key line
int indent = raw.length() - trimmed.length();
int depth = indent / 2; // assume 2-space indentation
// Pop stack back to current depth
while (pathStack.size() > depth) pathStack.pollLast();
// Parse key name (everything before ':')
String keyPart = trimmed.split(":")[0].trim();
pathStack.addLast(keyPart);
String fullPath = String.join(".", pathStack);
boolean isSection = trimmed.endsWith(":") || trimmed.matches(".*:\\s*$");
boolean isIgnored = isIgnored(fullPath, ignored);
if (!isIgnored && !diskKeys.contains(fullPath) && !isSection) {
// This key is missing on disk — inject it
// Find the best insertion point: after the last line that
// belongs to the parent section on disk
int insertAt = findInsertionPoint(result, pathStack, depth);
// Insert blank separator + comments + key line
List<String> block = new ArrayList<>();
if (insertAt == result.size() || !result.get(Math.max(0, insertAt - 1)).isBlank()) {
block.add("");
}
block.addAll(pendingComments);
block.add(raw);
result.addAll(insertAt, block);
}
// Clear pending comments regardless
pendingComments.clear();
}
return result;
}
/**
* Finds the line index where a missing key should be inserted.
* Inserts after the last line of its parent section.
*/
private static int findInsertionPoint(List<String> lines, Deque<String> pathStack, int depth) {
if (depth == 0) {
// Top-level key → append at end
return lines.size();
}
// Parent path
List<String> parts = new ArrayList<>(pathStack);
String parentKey = parts.get(depth - 1);
int parentIndent = (depth - 1) * 2;
// Find the parent key line in the result
int parentLine = -1;
for (int i = 0; i < lines.size(); i++) {
String l = lines.get(i);
int ind = l.length() - l.stripLeading().length();
if (ind == parentIndent && l.stripLeading().startsWith(parentKey + ":")) {
parentLine = i;
break;
}
}
if (parentLine < 0) return lines.size();
// Find end of parent section: next line with same or lower indent that isn't blank/comment
for (int i = parentLine + 1; i < lines.size(); i++) {
String l = lines.get(i);
if (l.isBlank() || l.stripLeading().startsWith("#")) continue;
int ind = l.length() - l.stripLeading().length();
if (ind <= parentIndent) return i;
}
return lines.size();
}
// ── IO helpers ─────────────────────────────────────────────────────────────
/** Reads a resource from the jar as a list of lines. Returns null if not found. */
@Nullable
private static List<String> readResource(Plugin plugin, String name) {
try (InputStream in = plugin.getResource(name)) {
if (in == null) return null;
return readLines(in);
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to read resource: " + name, ex);
return null;
}
}
private static List<String> readLines(File file) throws IOException {
try (BufferedReader r = new BufferedReader(
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
return readLines(r);
}
}
private static List<String> readLines(InputStream in) throws IOException {
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
return readLines(r);
}
}
private static List<String> readLines(BufferedReader reader) throws IOException {
List<String> lines = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) lines.add(line);
return lines;
}
private static void writeLines(File file, List<String> lines) throws IOException {
try (BufferedWriter w = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
for (int i = 0; i < lines.size(); i++) {
w.write(lines.get(i));
if (i < lines.size() - 1) w.newLine();
}
}
}
// ── Key extraction ─────────────────────────────────────────────────────────
/**
* Returns the flat set of dotted-path keys present in the file.
* Only leaf keys (not sections) are returned.
*/
private static Set<String> flatKeys(File file) throws IOException {
Set<String> keys = new LinkedHashSet<>();
Deque<String> stack = new ArrayDeque<>();
List<String> lines = readLines(file);
for (String raw : lines) {
String trimmed = raw.stripLeading();
if (trimmed.isBlank() || trimmed.startsWith("#")) continue;
int indent = raw.length() - trimmed.length();
int depth = indent / 2;
while (stack.size() > depth) stack.pollLast();
if (!trimmed.contains(":")) continue;
String keyPart = trimmed.split(":")[0].trim();
stack.addLast(keyPart);
boolean isSection = trimmed.matches("[^:]+:\\s*$");
if (!isSection) {
keys.add(String.join(".", stack));
}
}
return keys;
}
// ── Helpers ────────────────────────────────────────────────────────────────
private static Set<String> normalizedIgnored(@Nullable List<String> list) {
if (list == null || list.isEmpty()) return Collections.emptySet();
Set<String> result = new LinkedHashSet<>();
for (String s : list) result.add(s.toLowerCase(Locale.ROOT).trim());
return result;
}
private static boolean isIgnored(String fullPath, Set<String> ignored) {
String lower = fullPath.toLowerCase(Locale.ROOT);
for (String prefix : ignored) {
if (lower.equals(prefix) || lower.startsWith(prefix + ".")) return true;
}
return false;
}
private static int countNew(List<String> before, List<String> after) {
return Math.max(0, after.size() - before.size());
}
}

View file

@ -1,7 +0,0 @@
package dev.loki.loparkour.generator;
/**
* @deprecated Logic merged into {@link BlockPlacer}. Delete this file.
*/
@Deprecated
class BlockSelector {}

View file

@ -1,8 +0,0 @@
package dev.loki.loparkour.generator;
/**
* @deprecated Tick logic was merged back into {@link ParkourGenerator#tick()}.
* Delete this file.
*/
@Deprecated
class GeneratorTick {}

View file

@ -1,4 +1,4 @@
package dev.loki.loparkour.generator;
package dev.loki.loparkour.generator;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;

View file

@ -1,8 +0,0 @@
package dev.loki.loparkour.generator;
/**
* @deprecated Score logic lives in {@link GeneratorLifecycle}.
* This class was created but never wired in. Delete this file.
*/
@Deprecated
class ScoreManager {}

View file

@ -54,12 +54,11 @@ public class EffectManager {
);
viewers.forEach(p -> p.player.spawnParticle(
Option.PARTICLE_TYPE, center, 5, 0.1, 0.1, 0.1, 0,
Option.PARTICLE_DATA != null ? Option.PARTICLE_DATA.data() : null
Option.PARTICLE_DATA
));
}
case CIRCLE -> ParticleUtil.circle(
min.clone().add(0.5, 0.5, 0.5), max.getWorld(), Option.PARTICLE_TYPE,
viewers.stream().map(p -> p.player).toList(), 0.5, 8
min.clone().add(0.5, 0.5, 0.5), Option.PARTICLE_TYPE, 8, 1
);
case BOX -> ParticleUtil.box(
org.bukkit.util.BoundingBox.of(min, max), max.getWorld(), Option.PARTICLE_TYPE,

View file

@ -106,7 +106,7 @@ public class PAPIHook extends PlaceholderExpansion {
ParkourGenerator generator = pp.session.generator;
switch (params) {
case "score", "current_score" -> {
return Integer.toString(generator.score);
return Integer.toString(generator.state.score);
}
case "time", "current_time" -> {
return generator.getFormattedTime();
@ -134,7 +134,7 @@ public class PAPIHook extends PlaceholderExpansion {
String replaced = params.replace("score_until_", "");
int interval = Integer.parseInt(replaced);
if (interval > 0) {
return Integer.toString(interval - (generator.totalScore % interval)); // 100 - (5 % 100) = 95
return Integer.toString(interval - (generator.state.score % interval));
} else {
return "0";
}

View file

@ -46,7 +46,7 @@ public class ParkourSettingsMenu extends LPMenu {
open(pp);
})
.setItem(12, toggleItem(locale, "special_blocks", pp.useSpecialBlocks), e -> {
if (pp.session.generator.score == 0) {
if (pp.session.generator.state.score == 0) {
pp.useSpecialBlocks = !pp.useSpecialBlocks;
pp.updateGeneratorSettings(pp.session.generator);
}
@ -65,7 +65,7 @@ public class ParkourSettingsMenu extends LPMenu {
.setItem(19, localeItem(player, "settings.parkour_settings.styles.item"),
e -> openStyleMenu(pp))
.setItem(20, schemDiffItem(pp), e -> {
if (pp.session.generator.score == 0) {
if (pp.session.generator.state.score == 0) {
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
int idx = diffs.indexOf(pp.schematicDifficulty);
pp.schematicDifficulty = diffs.get((idx + 1) % diffs.size());

View file

@ -0,0 +1,20 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.session.Session;
import org.jetbrains.annotations.NotNull;
/**
* Generator for Elytra mode parkour
*/
public class ElytraGenerator extends ParkourGenerator {
public ElytraGenerator(@NotNull Session session) {
super(session);
}
@Override
public Mode getMode() {
return Modes.ELYTRA;
}
}

View file

@ -36,7 +36,7 @@ import java.util.function.BiConsumer;
*/
public class ParkourPlayer extends ParkourUser {
public static final Map<String, OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
public static final Map<String, PlayerSettingsManager.OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
public @Expose Double schematicDifficulty;
public @Expose Integer blockLead;

View file

@ -14,7 +14,7 @@ import java.util.function.BiConsumer;
*
* @since 5.0.0
*/
class PlayerSettingsManager {
public class PlayerSettingsManager {
private static final Map<String, OptionContainer> COLUMN_MAPPINGS = new HashMap<>();

View file

@ -92,7 +92,9 @@ public class UserRegistry {
}
public static int getJoinCount() {
return joinCount;
int count = joinCount;
joinCount = 0;
return count;
}
private static void resetScoreboard(ParkourUser user) {