From 865512d8f3d75f9aed65af6101e33ff7730f6a8e Mon Sep 17 00:00:00 2001 From: sun-dev Date: Tue, 18 Aug 2026 13:36:49 +0530 Subject: [PATCH] Split config into multiple files and add profile pruning - Split config into config.yml, menus.yml, and messages.yml - Add /profile admin prune to clean up inactive database records - Add periodic background auto-save for online players - Deduplicate skill integrations and menu helper code - Use native OumLib ConfirmMenu and ItemBuilder helpers - Bump version to 1.2.0 and add GitHub release action --- .github/workflows/release.yml | 68 +++ README.md | 444 +++++++------- pom.xml | 4 +- src/main/java/dev/oum/profile/OumProfile.java | 19 +- .../dev/oum/profile/ProfilePlaceholders.java | 4 +- .../java/dev/oum/profile/api/ProfileAPI.java | 20 + .../dev/oum/profile/command/Permissions.java | 2 +- .../oum/profile/command/ProfileCommand.java | 191 +++--- .../dev/oum/profile/config/MainConfig.java | 184 ++++++ .../dev/oum/profile/config/MenusConfig.java | 260 ++++++++ .../oum/profile/config/MessagesConfig.java | 139 +++++ .../dev/oum/profile/config/ProfileConfig.java | 557 ++---------------- .../oum/profile/config/ProfileStorage.java | 61 +- .../integration/IntegrationManager.java | 44 +- .../oum/profile/integration/SkillHandler.java | 13 + .../auraskills/AuraSkillsHandler.java | 16 +- .../profile/integration/jobs/JobsHandler.java | 16 +- .../integration/mcmmo/McMMOHandler.java | 16 +- .../dev/oum/profile/model/PlayerState.java | 12 +- .../dev/oum/profile/profile/ConfirmMenu.java | 102 ---- .../oum/profile/profile/ProfileListener.java | 30 +- .../oum/profile/profile/ProfileManager.java | 261 +++++--- .../dev/oum/profile/profile/ProfileMenu.java | 173 +++--- src/main/resources/paper-plugin.yml | 2 +- 24 files changed, 1401 insertions(+), 1237 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 src/main/java/dev/oum/profile/config/MainConfig.java create mode 100644 src/main/java/dev/oum/profile/config/MenusConfig.java create mode 100644 src/main/java/dev/oum/profile/config/MessagesConfig.java create mode 100644 src/main/java/dev/oum/profile/integration/SkillHandler.java delete mode 100644 src/main/java/dev/oum/profile/profile/ConfirmMenu.java diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..483dff5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Release Tag (e.g. v1.2.0)' + required: true + default: 'v1.2.0' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout OumProfile + uses: actions/checkout@v4 + + - name: Checkout OumLib + uses: actions/checkout@v4 + with: + repository: sun-mc-dev/oumlib + path: oumlib + ref: dev + token: ${{ secrets.GH_PAT || github.token }} + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build and Install OumLib + run: mvn clean install -DskipTests -f oumlib/pom.xml + + - name: Determine Release Tag + id: vars + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV + else + echo "TAG_NAME=${{ github.ref_name }}" >> $GITHUB_ENV + fi + + - name: Build OumProfile + run: | + mvn clean package -DskipTests + cp target/oumprofile-*.jar target/OumProfile-${{ env.TAG_NAME }}.jar + cd target + sha256sum OumProfile-${{ env.TAG_NAME }}.jar > checksums.txt + cd .. + + - name: Publish GitHub Release + run: | + gh release create ${{ env.TAG_NAME }} \ + target/OumProfile-${{ env.TAG_NAME }}.jar \ + target/checksums.txt \ + --title "OumProfile ${{ env.TAG_NAME }}" \ + --generate-notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index ad99b81..48fe37e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Each player profile stores: * **Movement States**: Flight capabilities (allowFlight and isFlying state). * **Optional Features**: Coordinate location (if saveLocation is enabled), Vault balance, LuckPerms group, mcMMO skills, AuraSkills, JobsReborn jobs, custom multi-currencies, playtime tracking, and vanilla Minecraft statistics. +* **Server Resilience**: Non-blocking periodic auto-save to protect against sudden crashes, plus administrative stale profile pruning (`/profile admin prune `) to keep databases compact. --- @@ -42,6 +43,7 @@ The plugin automatically integrates with the following if present on the server: * **mcMMO**: For per-profile skill level and experience synchronization. * **AuraSkills**: For per-profile skill level and experience synchronization. * **JobsReborn**: For per-profile job level and experience progression. +* **CombatLogX / PvPManager / DeluxeCombat**: Automatic combat tag detection for profile switch blocking. * **PlaceholderAPI & MiniPlaceholders**: For displaying profile statistics in chats, scoreboards, and tablists. --- @@ -96,6 +98,7 @@ The following placeholders are supported under the `oumprofile` namespace: | `/profile admin rename ` | Renames a profile for the specified player. | OP (requires `profiles.admin`) | | `/profile admin export ` | Exports a player's profile to a JSON file. | OP (requires `profiles.admin`) | | `/profile admin import ` | Imports a profile from a JSON file for a player. | OP (requires `profiles.admin`) | +| `/profile admin prune ` | Prunes all inactive profiles older than the given days. | OP (requires `profiles.admin`) | | `/profile debug` | Toggles debug logging in console. | OP (requires `profiles.admin`) | | `profiles.admin` | Access to administrative commands and config reload. | OP | | `profiles.create.*` | Permission to create profiles with any name. | OP | @@ -110,40 +113,74 @@ The following placeholders are supported under the `oumprofile` namespace: ## Configuration Reference -The `config.yml` file allows detailed configuration of storage backends, warmup checks, interface layouts, and messages: +OumProfile is designed with a clean, modular configuration architecture split into 3 dedicated files: +- **`config.yml`**: Core plugin settings, database persistence, profile switching mechanics, auto-save, and third-party integrations. +- **`menus.yml`**: Inventory GUI layouts, custom head textures, button patterns, items, lores, and confirmation dialogs. +- **`messages.yml`**: Localization, chat messages, alerts, countdown titles, and error notifications with full MiniMessage support. + +All configuration files support **live auto-reloading** without requiring a server restart. + +### 1. `config.yml` (Main Configuration) ```yaml # Enable detailed debug logging in console debug: false -# Profile switching settings -switching: - warmupEnabled: true - warmupSeconds: 5 - cancelOnMove: true - cancelOnDamage: true - cancelInCombat: true - combatTagDuration: 10 - switchCooldownSeconds: 10 - saveLocation: false - warmupTitleEnabled: true - warmupTitleText: "Switching Profile..." - warmupSubtitleText: "Do not move for s" - warmupSoundEnabled: true - warmupSoundKey: "block.note_block.hat" - warmupCompleteSoundKey: "entity.player.levelup" - warmupCancelSoundKey: "entity.villager.no" +# Name of the default profile created on first join +default-profile-name: "default" + +# Global date format pattern +date-format: "yyyy-MM-dd HH:mm" + +# Enable administrative alerts when players switch, create, or delete profiles +admin-alerts-enabled: true + +# Maximum character length for profile names +profile-name-max-length: 16 + +# Regex pattern for valid profile names +profile-name-regex: "[a-zA-Z0-9_-]+" + +# Max profile limits based on profiles.max. permission nodes +limit-tiers: + - 1 + - 3 + - 5 + - 10 + +# Periodic background auto-save settings for active player profiles +auto-save: + enabled: true + interval-minutes: 5 # Database storage settings (SQLite/MySQL) storage: - type: "sqlite" + type: "sqlite" # 'sqlite' or 'mysql' host: "localhost" port: 3306 database: "oumprofile" username: "root" password: "" -# LuckPerms group integration +# Profile switching mechanics, warmups, combat checks, and sounds +switching: + warmup-enabled: true + warmup-seconds: 5 + cancel-on-move: true + cancel-on-damage: true + cancel-in-combat: true + combat-tag-duration: 10 + switch-cooldown-seconds: 10 + save-location: false + warmup-title-enabled: true + warmup-title-text: "Switching Profile..." + warmup-subtitle-text: "Do not move for s" + warmup-sound-enabled: true + warmup-sound-key: "block.note_block.hat" + warmup-complete-sound-key: "entity.player.levelup" + warmup-cancel-sound-key: "entity.villager.no" + +# LuckPerms group synchronization luckperms: enabled: true @@ -156,9 +193,9 @@ economy: # Skill and Job synchronization settings skills: - mcmmoEnabled: true - auraSkillsEnabled: true - jobsEnabled: true + mcmmo-enabled: true + aura-skills-enabled: true + jobs-enabled: true # Vanilla statistics synchronization settings statistics: @@ -167,68 +204,11 @@ statistics: - "MOB_KILLS" - "DEATHS" - "JUMP" +``` + +### 2. `menus.yml` (GUI Layouts & Menus) -# Custom plugin messages (MiniMessage tags allowed) -messages: - profileNotFound: "Profile '' does not exist." - profileAlreadyActive: "You are already using that profile." - combatBlock: "You cannot switch profiles while in combat." - warmupStart: "Switching to in s..." - switchSuccess: "Switched to profile ." - noPermission: "You don't have permission to create a profile named ''." - createFail: "Could not create profile '' (limit reached or name exists)." - createSuccess: "Created profile ." - deleteFail: "Could not delete profile '' (active, last remaining, or not found)." - deleteSuccess: "Deleted profile ." - help: "OumProfile » /profile " - listHeader: "Your profiles ():" - listItemActive: " Active" - listItemInactive: " Last used " - currentProfile: "Active Profile: " - playerOnly: "This command must be run as a player." - noProfiles: "You have no profiles." - noActiveProfile: "No active profile found." - reloadSuccess: "Configuration reloaded successfully." - switchCooldown: "Please wait s before switching profiles again." - maxProfilesReached: "You have reached your maximum profile slot limit." - cannotDeleteActive: "You cannot delete your active profile." - cannotDeleteDefault: "You cannot delete your default profile." - invalidProfileName: "Profile name must not be empty or contain spaces." - profileCreationCancelled: "Profile creation cancelled." - profileCreationTimedOut: "Profile creation timed out." - playerNotFound: "Player not found." - adminOpenSuccess: "Opened profile menu for ." - adminListHeader: "Profiles for ():" - adminCreateSuccess: "Successfully created profile for ." - adminCreateFail: "Failed to create profile (already exists or limit reached)." - adminSwitchSuccess: "Forced to switch to profile ." - adminSwitchFailNoProfile: "Player does not have a profile named ''." - adminDeleteSuccess: "Successfully deleted profile for ." - adminDeleteFail: "Failed to delete profile (active, last remaining, or not found)." - adminAlertSwitch: "ALERT | switched to profile " - adminAlertCreate: "ALERT | created profile " - adminAlertDelete: "ALERT | deleted profile " - alertsEnabled: "Profile alerts enabled." - alertsDisabled: "Profile alerts disabled." - warmupCancelledMove: "Profile switch cancelled because you moved." - warmupCancelledDamage: "Profile switch cancelled because you took damage." - warmupCancelledGeneric: "Profile switch cancelled." - debugEnabled: "Debug mode enabled." - debugDisabled: "Debug mode disabled." - renameSuccess: "Renamed profile to ." - renameFail: "Could not rename profile ''." - cannotRenameDefault: "You cannot rename the default profile." - adminRenameSuccess: "Renamed profile to for ." - adminRenameFail: "Failed to rename profile for player." - adminAlertRename: "ALERT | renamed profile to " - profileNameTooLong: "Profile name must be at most characters." - profileNameInvalidChars: "Profile name contains invalid characters. Only letters, numbers, hyphens and underscores are allowed." - exportSuccess: "Exported profile to file." - importSuccess: "Imported profile for ." - importFail: "Failed to import profile from file." - - -# GUI menus and chat input settings +```yaml gui: title: "Select a Profile" rows: 3 @@ -236,25 +216,27 @@ gui: - "#########" - " PPPPP " - "####C####" - createButtonMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWZmMzE0MzFkNjQ1ODdmZjZlZjk4YzA2NzU4MTA2ODFmOGMxM2JmOTZmNTFkOWNiMDdlZDc4NTJiMmZmZDEifX19" - createButtonMaterialLimitReached: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODE5OWI1ZWUzMjBlNzk5N2Q5MWJiNWY4NjY1ZjNkMzJhZTQ5MjBlMDNjNmIzZDliN2VlY2E2OTcxMTk5OTcifX19" - createButtonName: "Create New Profile" - createButtonNameLimitReached: "Profile Limit Reached" - createButtonLore: + profile-slot-char: "P" + create-button-slot-char: "C" + create-button-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWZmMzE0MzFkNjQ1ODdmZjZlZjk4YzA2NzU4MTA2ODFmOGMxM2JmOTZmNTFkOWNiMDdlZDc4NTJiMmZmZDEifX19" + create-button-material-limit-reached: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODE5OWI1ZWUzMjBlNzk5N2Q5MWJiNWY4NjY1ZjNkMzJhZTQ5MjBlMDNjNmIzZDliN2VlY2E2OTcxMTk5OTcifX19" + create-button-name: "Create New Profile" + create-button-name-limit-reached: "Profile Limit Reached" + create-button-lore: - "Slots: / " - "" - "Click to start profile creation" - createButtonLoreLimitReached: + create-button-lore-limit-reached: - "Slots: / " - "" - "Purchase more slots on our store" - activeProfileMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjdmYWFlMWQxOTgzNmJkMDc4NTQyNmU0ZmQyOGFhNjNhMzgxZTllNzE0OTU1OWVlNmIyYTUwOTk5NWJiY2ZkMiJ9fX0=" - inactiveProfileMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDVjNmRjMmJiZjUxYzM2Y2ZjNzcxNDU4NWE2YTU2ODNlZjJiMTRkNDdkOGZmNzE0NjU0YTg5M2Y1ZGE2MjIifX19" - emptySlotMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDZiYTYzMzQ0ZjQ5ZGQxYzRmNTQ4OGU5MjZiZjNkOWUyYjI5OTE2YTZjNTBkNjEwYmI0MGE1MjczZGM4YzgyIn19fQ==" - activeProfileName: " (Active)" - inactiveProfileName: "" - emptySlotName: "Empty Slot" - activeProfileLore: + active-profile-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjdmYWFlMWQxOTgzNmJkMDc4NTQyNmU0ZmQyOGFhNjNhMzgxZTllNzE0OTU1OWVlNmIyYTUwOTk5NWJiY2ZkMiJ9fX0=" + inactive-profile-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDVjNmRjMmJiZjUxYzM2Y2ZjNzcxNDU4NWE2YTU2ODNlZjJiMTRkNDdkOGZmNzE0NjU0YTg5M2Y1ZGE2MjIifX19" + empty-slot-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDZiYTYzMzQ0ZjQ5ZGQxYzRmNTQ4OGU5MjZiZjNkOWUyYjI5OTE2YTZjNTBkNjEwYmI0MGE1MjczZGM4YzgyIn19fQ==" + active-profile-name: " (Active)" + inactive-profile-name: "" + empty-slot-name: "Empty Slot" + active-profile-lore: - "━━━━━━━━━━━━━━━━━━━━━" - "Created: " - "Last Used: " @@ -264,7 +246,7 @@ gui: - "Jobs: " - "━━━━━━━━━━━━━━━━━━━━━" - "Currently Active" - inactiveProfileLore: + inactive-profile-lore: - "━━━━━━━━━━━━━━━━━━━━━" - "Created: " - "Last Used: " @@ -275,24 +257,21 @@ gui: - "━━━━━━━━━━━━━━━━━━━━━" - "Left-Click to switch" - "Right-Click to delete" - borderMaterial: "GRAY_STAINED_GLASS_PANE" - borderName: " " - promptMessage: "Profile Creation\nType a name in chat for your new profile.\nType cancel to return." - cancelWord: "cancel" - textInputTimeoutSeconds: 30 - profileSlotChar: "P" - createButtonSlotChar: "C" - openSoundEnabled: true - openSoundKey: "block.chest.open" - clickSoundEnabled: true - clickSoundKey: "ui.button.click" - errorSoundEnabled: true - errorSoundKey: "entity.villager.no" - closeSoundEnabled: true - closeSoundKey: "block.chest.close" - -# Confirmation GUI settings for deleting profiles -confirmDelete: + border-material: "GRAY_STAINED_GLASS_PANE" + border-name: " " + prompt-message: "Type a profile name in chat:" + text-input-timeout-seconds: 15 + cancel-word: "cancel" + open-sound-enabled: true + open-sound-key: "block.chest.open" + click-sound-enabled: true + click-sound-key: "ui.button.click" + close-sound-enabled: true + close-sound-key: "block.chest.close" + error-sound-enabled: true + error-sound-key: "entity.villager.no" + +confirm-delete: enabled: true title: "Confirm Deleting " rows: 3 @@ -300,25 +279,24 @@ confirmDelete: - "#########" - " C D " - "#########" - confirmSlotChar: "C" - confirmMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==" - confirmName: "Confirm Deletion" - confirmLore: + confirm-slot-char: "C" + confirm-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==" + confirm-name: "Confirm Deletion" + confirm-lore: - "Clicking here will permanently" - "delete the profile ." - "" - "WARNING: This cannot be undone!" - denySlotChar: "D" - denyMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=" - denyName: "Cancel" - denyLore: + deny-slot-char: "D" + deny-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=" + deny-name: "Cancel" + deny-lore: - "Click to keep your profile" - "and return to the menu." - borderMaterial: "GRAY_STAINED_GLASS_PANE" - borderName: " " + border-material: "GRAY_STAINED_GLASS_PANE" + border-name: " " -# Confirmation GUI settings for creating profiles -confirmCreate: +confirm-create: enabled: true title: "Confirm Creating " rows: 3 @@ -326,73 +304,117 @@ confirmCreate: - "#########" - " C D " - "#########" - confirmSlotChar: "C" - confirmMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=" - confirmName: "Confirm Creation" - confirmLore: + confirm-slot-char: "C" + confirm-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=" + confirm-name: "Confirm Creation" + confirm-lore: - "Click here to create" - "profile ." - denySlotChar: "D" - denyMaterial: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==" - denyName: "Cancel" - denyLore: + deny-slot-char: "D" + deny-material: "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==" + deny-name: "Cancel" + deny-lore: - "Click to cancel creation" - "and return to the menu." - borderMaterial: "GRAY_STAINED_GLASS_PANE" - borderName: " " - -# Max profile limits based on oumprofile.max. permission nodes -limitTiers: - - 1 - - 3 - - 5 - - 10 - -# Name of the default profile created on first join -defaultProfileName: "default" + border-material: "GRAY_STAINED_GLASS_PANE" + border-name: " " +``` -# Global date format pattern -dateFormat: "yyyy-MM-dd HH:mm" +### 3. `messages.yml` (Localization & Chat Messages) -# Enable administrative alerts when players switch, create, or delete profiles -adminAlertsEnabled: true - -# Maximum character length for profile names -profileNameMaxLength: 16 - -# Regex pattern for valid profile names -profileNameRegex: "[a-zA-Z0-9_-]+" +```yaml +profile-not-found: "Profile '' does not exist." +profile-already-active: "You are already using that profile." +combat-block: "You cannot switch profiles while in combat." +warmup-start: "Switching to in s..." +switch-success: "Switched to profile ." +no-permission: "You don't have permission to create a profile named ''." +create-fail: "Could not create profile '' (limit reached or name exists)." +create-success: "Created profile ." +delete-fail: "Could not delete profile '' (active, last remaining, or not found)." +delete-success: "Deleted profile ." +help: "OumProfile » /profile " +list-header: "Your profiles ():" +list-item-active: " Active" +list-item-inactive: " Last used " +current-profile: "Active Profile: " +player-only: "This command can only be executed by players." +no-profiles: "You do not have any profiles yet." +no-active-profile: "You do not have an active profile loaded." +reload-success: "Configuration reloaded successfully." +switch-cooldown: "You must wait before switching profiles again." +max-profiles-reached: "You have reached the maximum number of profiles allowed ()." +cannot-delete-active: "You cannot delete your active profile. Switch to another profile first." +cannot-delete-default: "You cannot delete the default profile." +invalid-profile-name: "Profile name cannot be empty or contain spaces." +profile-creation-cancelled: "Profile creation cancelled." +profile-creation-timed-out: "Profile creation timed out." +player-not-found: "Player not found." +admin-open-success: "Opened profile menu for ." +admin-list-header: "Profiles for ():" +admin-create-success: "Created profile for ." +admin-create-fail: "Failed to create profile for player (limit reached or name exists)." +admin-switch-success: "Switched to profile ." +admin-switch-fail-no-profile: "Player does not have profile ''." +admin-delete-success: "Deleted profile for ." +admin-delete-fail: "Failed to delete profile for player (active, last profile, or not found)." +admin-alert-switch: "ALERT | switched to profile " +admin-alert-create: "ALERT | created profile " +admin-alert-delete: "ALERT | deleted profile " +alerts-enabled: "Profile alerts enabled." +alerts-disabled: "Profile alerts disabled." +warmup-cancelled-move: "Profile switch cancelled because you moved." +warmup-cancelled-damage: "Profile switch cancelled because you took damage." +warmup-cancelled-generic: "Profile switch cancelled." +debug-enabled: "Debug mode enabled." +debug-disabled: "Debug mode disabled." +rename-success: "Renamed profile to ." +rename-fail: "Could not rename profile ''." +cannot-rename-default: "You cannot rename the default profile." +admin-rename-success: "Renamed profile to for ." +admin-rename-fail: "Failed to rename profile for player." +admin-alert-rename: "ALERT | renamed profile to " +profile-name-too-long: "Profile name must be at most characters." +profile-name-invalid-chars: "Profile name contains invalid characters. Only letters, numbers, hyphens and underscores are allowed." +export-success: "Exported profile to file." +import-success: "Imported profile for ." +import-fail: "Failed to import profile from file." +admin-prune-success: "Successfully pruned inactive profile(s) older than days." +admin-prune-none: "No inactive profiles older than days were found to prune." +invalid-days: "Please specify a valid number of days (greater than 0)." ``` ### Configuration Options -#### Global Settings - -| Option | Type | Default | Description | -|:-----------------------|:--------------|:-------------------|:-----------------------------------------------------------------------------------| -| `debug` | Boolean | `false` | Enable detailed debug logging in the server console. | -| `defaultProfileName` | String | `default` | Name of the initial profile created automatically when a player first joins. | -| `dateFormat` | String | `yyyy-MM-dd HH:mm` | Date format used for displaying profile creation and last used timestamps. | -| `adminAlertsEnabled` | Boolean | `true` | Broadcast profile actions (create, delete, switch, rename) to administrators. | -| `limitTiers` | List | `[1, 3, 5, 10]` | Profile slot limit thresholds based on permission nodes (e.g. `oumprofile.max.5`). | -| `profileNameMaxLength` | Integer | `16` | Maximum character length allowed for profile names. | -| `profileNameRegex` | String | `[a-zA-Z0-9_-]+` | Regex pattern that profile names must match. | - -#### Switching Settings (`switching`) - -| Option | Type | Default | Description | -|:------------------------|:--------|:--------|:-----------------------------------------------------------------------------------| -| `warmupEnabled` | Boolean | `true` | If true, players must stand still for a warmup duration before switching profiles. | -| `warmupSeconds` | Integer | `5` | Warmup countdown duration in seconds. | -| `cancelOnMove` | Boolean | `true` | Cancel the switch warmup if the player moves. | -| `cancelOnDamage` | Boolean | `true` | Cancel the switch warmup if the player takes damage. | -| `cancelInCombat` | Boolean | `true` | Cancel the switch warmup if the player is in combat. | -| `combatTagDuration` | Integer | `10` | Duration in seconds that a player remains tagged in combat. | -| `switchCooldownSeconds` | Integer | `10` | Cooldown period in seconds before a player can switch profiles again. | -| `saveLocation` | Boolean | `false` | Save and restore player coordinates per-profile. | -| `warmupTitleEnabled` | Boolean | `true` | Show title/subtitle countdown during warmup. | - -#### Storage Settings (`storage`) +#### Global Settings (`config.yml`) + +| Option | Type | Default | Description | +|:--------------------------|:--------------|:-------------------|:------------------------------------------------------------------------------------| +| `debug` | Boolean | `false` | Enable detailed debug logging in the server console. | +| `default-profile-name` | String | `default` | Name of the initial profile created automatically when a player first joins. | +| `date-format` | String | `yyyy-MM-dd HH:mm` | Date format used for displaying profile creation and last used timestamps. | +| `admin-alerts-enabled` | Boolean | `true` | Broadcast profile actions (create, delete, switch, rename) to administrators. | +| `limit-tiers` | List | `[1, 3, 5, 10]` | Profile slot limit thresholds based on permission nodes (e.g. `profiles.max.5`). | +| `profile-name-max-length` | Integer | `16` | Maximum character length allowed for profile names. | +| `profile-name-regex` | String | `[a-zA-Z0-9_-]+` | Regex pattern that profile names must match. | +| `auto-save.enabled` | Boolean | `true` | Enable periodic background auto-saving of active player profiles. | +| `auto-save.interval-minutes` | Integer | `5` | Time in minutes between automatic profile saves. | + +#### Switching Settings (`switching` in `config.yml`) + +| Option | Type | Default | Description | +|:---------------------------|:--------|:--------|:-----------------------------------------------------------------------------------| +| `warmup-enabled` | Boolean | `true` | If true, players must stand still for a warmup duration before switching profiles. | +| `warmup-seconds` | Integer | `5` | Warmup countdown duration in seconds. | +| `cancel-on-move` | Boolean | `true` | Cancel the switch warmup if the player moves. | +| `cancel-on-damage` | Boolean | `true` | Cancel the switch warmup if the player takes damage. | +| `cancel-in-combat` | Boolean | `true` | Cancel the switch warmup if the player is in combat. | +| `combat-tag-duration` | Integer | `10` | Duration in seconds that a player remains tagged in combat. | +| `switch-cooldown-seconds` | Integer | `10` | Cooldown period in seconds before a player can switch profiles again. | +| `save-location` | Boolean | `false` | Save and restore player coordinates per-profile. | +| `warmup-title-enabled` | Boolean | `true` | Show title/subtitle countdown during warmup. | + +#### Storage Settings (`storage` in `config.yml`) | Option | Type | Default | Description | |:-----------|:--------|:-------------|:---------------------------------------------| @@ -403,29 +425,31 @@ profileNameRegex: "[a-zA-Z0-9_-]+" | `username` | String | `root` | Username for MySQL database authentication. | | `password` | String | `""` | Password for MySQL database authentication. | -#### Integrations Settings - -| Option | Type | Default | Description | -|:---------------------------|:-------------|:----------------------------------|:-----------------------------------------------------| -| `luckperms.enabled` | Boolean | `true` | Synchronize LuckPerms permission groups per-profile. | -| `economy.enabled` | Boolean | `true` | Enable per-profile multi-currency balances. | -| `economy.currencies` | List | `["vault", "playerpoints"]` | Currencies synchronized per-profile. | -| `skills.mcmmoEnabled` | Boolean | `true` | Synchronize mcMMO level and XP per-profile. | -| `skills.auraSkillsEnabled` | Boolean | `true` | Synchronize AuraSkills level and XP per-profile. | -| `skills.jobsEnabled` | Boolean | `true` | Synchronize JobsReborn job level and XP per-profile. | -| `statistics.enabled` | Boolean | `true` | Synchronize vanilla Minecraft statistics. | -| `statistics.tracked` | List | `["MOB_KILLS", "DEATHS", "JUMP"]` | Vanilla statistics tracked. | - -#### GUI Settings (`gui`) - -| Option | Type | Description | -|:--------------------------|:-------------|:----------------------------------------------------------------------------------------------------| -| `title` | String | Title of the profile inventory menu (supports MiniMessage). | -| `rows` | Integer | Number of rows in the GUI grid (1-6). | -| `pattern` | List | Character pattern defining the layout (e.g. `P` for profile items, `C` for creation button). | -| `activeProfileMaterial` | String | Item material/texture for the currently active profile (supports `head:` or `head:`). | -| `inactiveProfileMaterial` | String | Item material/texture for inactive profiles. | -| `emptySlotMaterial` | String | Item material/texture for unfilled profile slots. | +#### Integrations Settings (`config.yml`) + +| Option | Type | Default | Description | +|:----------------------------|:-------------|:----------------------------------|:-----------------------------------------------------| +| `luckperms.enabled` | Boolean | `true` | Synchronize LuckPerms permission groups per-profile. | +| `economy.enabled` | Boolean | `true` | Enable per-profile multi-currency balances. | +| `economy.currencies` | List | `["vault", "playerpoints"]` | Currencies synchronized per-profile. | +| `skills.mcmmo-enabled` | Boolean | `true` | Synchronize mcMMO level and XP per-profile. | +| `skills.aura-skills-enabled`| Boolean | `true` | Synchronize AuraSkills level and XP per-profile. | +| `skills.jobs-enabled` | Boolean | `true` | Synchronize JobsReborn job level and XP per-profile. | +| `statistics.enabled` | Boolean | `true` | Synchronize vanilla Minecraft statistics. | +| `statistics.tracked` | List | `["MOB_KILLS", "DEATHS", "JUMP"]` | Vanilla statistics tracked. | + +#### GUI Settings (`menus.yml`) + +| Option | Type | Description | +|:----------------------------|:-------------|:-----------------------------------------------------------------------------------------------------| +| `title` | String | Title of the profile inventory menu (supports MiniMessage). | +| `rows` | Integer | Number of rows in the GUI grid (1-6). | +| `pattern` | List | Character pattern defining the layout (e.g. `P` for profile items, `C` for creation button). | +| `active-profile-material` | String | Item material/texture for the currently active profile (supports `head:` or custom bridges).| +| `inactive-profile-material` | String | Item material/texture for inactive profiles. | +| `empty-slot-material` | String | Item material/texture for unfilled profile slots. | +| `confirm-delete.enabled` | Boolean | Toggle confirmation dialog before deleting profiles. | +| `confirm-create.enabled` | Boolean | Toggle confirmation dialog before creating profiles. | ##### GUI Lore Placeholders @@ -494,6 +518,10 @@ public class OumProfileAPIExample { double pvpBalance = ProfileAPI.getProfileBalance(uuid, "pvp"); ProfileAPI.setProfileBalance(uuid, "pvp", 5000.0); + // Maintenance & auto-save + ProfileAPI.saveAllOnline(); + ProfileAPI.pruneInactiveProfiles(180); + // Read integrated stats, playtime, and plugin data long playtime = ProfileAPI.getProfilePlaytimeSeconds(uuid, "pvp"); Map mcmmo = ProfileAPI.getProfileMcMMO(uuid, "pvp"); diff --git a/pom.xml b/pom.xml index 9e60001..1d6d0e7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ dev.oum oumprofile - 1.2-SNAPSHOT + 1.2.0 jar @@ -43,7 +43,7 @@ dev.oum oumlib-core - 1.0.8 + 1.0.9 compile diff --git a/src/main/java/dev/oum/profile/OumProfile.java b/src/main/java/dev/oum/profile/OumProfile.java index a7f8a04..e248d48 100644 --- a/src/main/java/dev/oum/profile/OumProfile.java +++ b/src/main/java/dev/oum/profile/OumProfile.java @@ -1,7 +1,6 @@ package dev.oum.profile; import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.config.ConfigManager; import dev.oum.oumlib.text.Text; import dev.oum.profile.api.ProfileAPI; import dev.oum.profile.command.ProfileCommand; @@ -22,20 +21,22 @@ public final class OumProfile extends JavaPlugin { public void onEnable() { OumLib.init(this); - ConfigManager configManager = ConfigManager.of(ProfileConfig.class, - "config.yml", ProfileConfig::defaults) - .onReload(cfg -> OumLib.setDebug(cfg.debug())) - .enableAutoReload(); + ProfileConfig config = ProfileConfig.create(() -> { + if (manager != null) { + OumLib.setDebug(manager.config().main().debug()); + manager.startAutoSave(); + } + }); - OumLib.setDebug(configManager.get().debug()); + OumLib.setDebug(config.main().debug()); - storage = new ProfileStorage(configManager.get().storage()); - manager = new ProfileManager(configManager, storage); + storage = new ProfileStorage(config.main().storage()); + manager = new ProfileManager(config, storage); ProfileAPI.init(manager); ProfilePlaceholders.register(manager); - new ProfileListener(manager, configManager); + new ProfileListener(manager); new ProfileCommand(manager).register(); List.of( diff --git a/src/main/java/dev/oum/profile/ProfilePlaceholders.java b/src/main/java/dev/oum/profile/ProfilePlaceholders.java index 7d56d8f..7eda495 100644 --- a/src/main/java/dev/oum/profile/ProfilePlaceholders.java +++ b/src/main/java/dev/oum/profile/ProfilePlaceholders.java @@ -1,7 +1,7 @@ package dev.oum.profile; +import dev.oum.oumlib.text.Format; import dev.oum.oumlib.text.placeholder.PlaceholderRegistry; -import dev.oum.oumlib.util.Format; import dev.oum.profile.integration.SkillData; import dev.oum.profile.profile.ProfileManager; import org.bukkit.entity.Player; @@ -67,7 +67,7 @@ public static void register(@NonNull ProfileManager manager) { return Format.duration(Duration.ofSeconds(base + elapsed)); }); - var config = manager.configManager().get(); + var config = manager.config().main(); if (config.economy() != null && config.economy().currencies() != null) { for (String currency : config.economy().currencies()) { registerCurrencyPlaceholder(currency, manager); diff --git a/src/main/java/dev/oum/profile/api/ProfileAPI.java b/src/main/java/dev/oum/profile/api/ProfileAPI.java index a0dbca4..87050ec 100644 --- a/src/main/java/dev/oum/profile/api/ProfileAPI.java +++ b/src/main/java/dev/oum/profile/api/ProfileAPI.java @@ -270,4 +270,24 @@ public static long getProfilePlaytimeSeconds(@NonNull UUID uuid, @NonNull String public static boolean renameProfile(@NonNull Player player, @NonNull String oldName, @NonNull String newName) { return manager().renameProfile(player, oldName, newName); } + + /** + * Triggers an asynchronous batch save of all online players' active profile states to database storage. + * + * @return A Promise completing when all profile saves have concluded. + */ + public static @NonNull Promise saveAllOnline() { + return manager().saveAllOnline(); + } + + /** + * Prunes all offline player profiles from the database that haven't been used in the given number of days. + * Profiles belonging to currently online players are preserved. + * + * @param days Inactivity threshold in days (must be greater than 0). + * @return A Promise completing with the number of deleted profile rows. + */ + public static @NonNull Promise pruneInactiveProfiles(int days) { + return manager().pruneInactiveProfiles(days); + } } \ No newline at end of file diff --git a/src/main/java/dev/oum/profile/command/Permissions.java b/src/main/java/dev/oum/profile/command/Permissions.java index 40a83e3..88b7165 100644 --- a/src/main/java/dev/oum/profile/command/Permissions.java +++ b/src/main/java/dev/oum/profile/command/Permissions.java @@ -1,6 +1,6 @@ package dev.oum.profile.command; -import dev.oum.oumlib.util.Permission; +import dev.oum.oumlib.bridge.permission.Permission; public final class Permissions { diff --git a/src/main/java/dev/oum/profile/command/ProfileCommand.java b/src/main/java/dev/oum/profile/command/ProfileCommand.java index 65dd0d4..87c7b42 100644 --- a/src/main/java/dev/oum/profile/command/ProfileCommand.java +++ b/src/main/java/dev/oum/profile/command/ProfileCommand.java @@ -15,8 +15,6 @@ import java.io.File; import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -29,7 +27,6 @@ public ProfileCommand(@NonNull ProfileManager manager) { this.manager = manager; } - @SuppressWarnings("ResultOfMethodCallIgnored") public void register() { Commands.create("profile") .aliases("profiles", "prof") @@ -59,7 +56,8 @@ public void register() { .subcommand(s -> s.label("debug") .permission(Permissions.ADMIN) .executes(this::onDebug)) - .subcommand(s -> s.label("help").executes(this::onHelp)) + .subcommand(s -> s.label("help") + .executes(this::onHelp)) .subcommand(admin -> admin.label("admin") .permission(Permissions.ADMIN) .subcommand(s -> { @@ -121,6 +119,10 @@ public void register() { .argument(Arguments.word("file").suggests(ctx -> this.suggestExportFiles())) .executes(ctx -> this.onAdminImport(ctx, targetArg)); }) + .subcommand(s -> s.label("prune") + .argument(Arguments.integer("days")) + .executes(this::onAdminPrune) + ) ) .executes(this::onGui) .register(); @@ -143,23 +145,13 @@ public void register() { return names; } - private @NonNull DateTimeFormatter dateFormatter() { - try { - return DateTimeFormatter.ofPattern(manager.configManager().get().dateFormat()) - .withZone(ZoneId.systemDefault()); - } catch (IllegalArgumentException e) { - return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") - .withZone(ZoneId.systemDefault()); - } - } - private void onHelp(@NonNull CommandContext ctx) { - Text.send(ctx.sender(), manager.configManager().get().messages().help()); + Text.send(ctx.sender(), manager.config().messages().help()); } private void onList(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); @@ -167,62 +159,45 @@ private void onList(@NonNull CommandContext ctx) { String active = manager.getActiveProfileName(player.getUniqueId()); if (profiles.isEmpty()) { - Text.send(player, manager.configManager().get().messages().noProfiles()); + Text.send(player, manager.config().messages().noProfiles()); return; } - Text.send(player, manager.configManager().get().messages().listHeader(), "count", String.valueOf(profiles.size())); + Text.send(player, manager.config().messages().listHeader(), "count", String.valueOf(profiles.size())); for (ProfileData data : profiles.values()) { boolean isActive = data.name().equals(active); - String format = isActive ? manager.configManager().get().messages().listItemActive() - : manager.configManager().get().messages().listItemInactive(); + String format = isActive ? manager.config().messages().listItemActive() + : manager.config().messages().listItemInactive(); Text.send(player, format, "name", data.name(), - "date", dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))); + "date", manager.dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))); } } private void onCurrent(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); String active = manager.getActiveProfileName(player.getUniqueId()); if (active == null) { - Text.send(player, manager.configManager().get().messages().noActiveProfile()); + Text.send(player, manager.config().messages().noActiveProfile()); return; } - Text.send(player, manager.configManager().get().messages().currentProfile(), "name", active); + Text.send(player, manager.config().messages().currentProfile(), "name", active); } private void onCreate(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); String name = ctx.args().getString("name"); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); - ProfileManager.NameValidation validation = manager.validateProfileName(name); - switch (validation) { - case EMPTY -> { - Text.send(player, msg.invalidProfileName()); - return; - } - case TOO_LONG -> { - Text.send(player, msg.profileNameTooLong(), "max", - String.valueOf(manager.configManager().get().profileNameMaxLength())); - return; - } - case INVALID_CHARS -> { - Text.send(player, msg.profileNameInvalidChars()); - return; - } - default -> { - } - } + if (!manager.checkNameValidation(player, name)) return; if (!player.hasPermission(Permissions.CREATE_PREFIX + name) && !player.hasPermission(Permissions.CREATE_ALL)) { Text.send(player, msg.noPermission(), "name", name); @@ -240,7 +215,7 @@ private void onCreate(@NonNull CommandContext ctx) { private void onSwitch(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); @@ -250,59 +225,42 @@ private void onSwitch(@NonNull CommandContext ctx) { private void onDelete(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); String name = ctx.args().getString("name"); - if (name.equalsIgnoreCase(manager.configManager().get().defaultProfileName())) { - Text.send(player, manager.configManager().get().messages().cannotDeleteDefault()); + if (name.equalsIgnoreCase(manager.config().main().defaultProfileName())) { + Text.send(player, manager.config().messages().cannotDeleteDefault()); return; } boolean deleted = manager.deleteProfile(player, name); if (!deleted) { - Text.send(player, manager.configManager().get().messages().deleteFail(), "name", name); + Text.send(player, manager.config().messages().deleteFail(), "name", name); return; } - Text.send(player, manager.configManager().get().messages().deleteSuccess(), "name", name); + Text.send(player, manager.config().messages().deleteSuccess(), "name", name); } private void onRename(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); String oldName = ctx.args().getString("old"); String newName = ctx.args().getString("new"); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); - if (oldName.equalsIgnoreCase(manager.configManager().get().defaultProfileName())) { + if (oldName.equalsIgnoreCase(manager.config().main().defaultProfileName())) { Text.send(player, msg.cannotRenameDefault()); return; } - ProfileManager.NameValidation validation = manager.validateProfileName(newName); - switch (validation) { - case EMPTY -> { - Text.send(player, msg.invalidProfileName()); - return; - } - case TOO_LONG -> { - Text.send(player, msg.profileNameTooLong(), "max", - String.valueOf(manager.configManager().get().profileNameMaxLength())); - return; - } - case INVALID_CHARS -> { - Text.send(player, msg.profileNameInvalidChars()); - return; - } - default -> { - } - } + if (!manager.checkNameValidation(player, newName)) return; boolean renamed = manager.renameProfile(player, oldName, newName); if (renamed) { @@ -313,13 +271,13 @@ private void onRename(@NonNull CommandContext ctx) { } private void onReload(@NonNull CommandContext ctx) { - manager.configManager().reload(); - Text.send(ctx.sender(), manager.configManager().get().messages().reloadSuccess()); + manager.config().reload(); + Text.send(ctx.sender(), manager.config().messages().reloadSuccess()); } private void onGui(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } new ProfileMenu(manager).open(ctx.playerOrThrow()); @@ -338,7 +296,7 @@ private void onGui(@NonNull CommandContext ctx) { private void onAdminOpen(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -349,7 +307,7 @@ private void onAdminOpen(@NonNull CommandContext ctx, @NonNull Argument targe private void onAdminList(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -368,37 +326,20 @@ private void onAdminList(@NonNull CommandContext ctx, @NonNull Argument targe String format = isActive ? msg.listItemActive() : msg.listItemInactive(); Text.send(ctx.sender(), format, "name", data.name(), - "date", dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))); + "date", manager.dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))); } } private void onAdminCreate(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; } String profileName = ctx.args().getString("profile"); - ProfileManager.NameValidation validation = manager.validateProfileName(profileName); - switch (validation) { - case EMPTY -> { - Text.send(ctx.sender(), msg.invalidProfileName()); - return; - } - case TOO_LONG -> { - Text.send(ctx.sender(), msg.profileNameTooLong(), "max", - String.valueOf(manager.configManager().get().profileNameMaxLength())); - return; - } - case INVALID_CHARS -> { - Text.send(ctx.sender(), msg.profileNameInvalidChars()); - return; - } - default -> { - } - } + if (!manager.checkNameValidation(ctx.sender(), profileName)) return; boolean created = manager.createProfile(target, profileName); if (created) { @@ -410,7 +351,7 @@ private void onAdminCreate(@NonNull CommandContext ctx, @NonNull Argument tar private void onAdminSwitch(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -428,7 +369,7 @@ private void onAdminSwitch(@NonNull CommandContext ctx, @NonNull Argument tar private void onAdminDelete(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -444,7 +385,7 @@ private void onAdminDelete(@NonNull CommandContext ctx, @NonNull Argument tar private void onAdminRename(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -452,29 +393,12 @@ private void onAdminRename(@NonNull CommandContext ctx, @NonNull Argument tar String oldName = ctx.args().getString("old"); String newName = ctx.args().getString("new"); - if (oldName.equalsIgnoreCase(manager.configManager().get().defaultProfileName())) { + if (oldName.equalsIgnoreCase(manager.config().main().defaultProfileName())) { Text.send(ctx.sender(), msg.cannotRenameDefault()); return; } - ProfileManager.NameValidation validation = manager.validateProfileName(newName); - switch (validation) { - case EMPTY -> { - Text.send(ctx.sender(), msg.invalidProfileName()); - return; - } - case TOO_LONG -> { - Text.send(ctx.sender(), msg.profileNameTooLong(), "max", - String.valueOf(manager.configManager().get().profileNameMaxLength())); - return; - } - case INVALID_CHARS -> { - Text.send(ctx.sender(), msg.profileNameInvalidChars()); - return; - } - default -> { - } - } + if (!manager.checkNameValidation(ctx.sender(), newName)) return; boolean renamed = manager.renameProfile(target, oldName, newName); if (renamed) { @@ -486,7 +410,7 @@ private void onAdminRename(@NonNull CommandContext ctx, @NonNull Argument tar private void onAdminExport(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -511,7 +435,7 @@ private void onAdminExport(@NonNull CommandContext ctx, @NonNull Argument tar private void onAdminImport(@NonNull CommandContext ctx, @NonNull Argument targetArg) { Player target = (Player) ctx.args().get(targetArg); - var msg = manager.configManager().get().messages(); + var msg = manager.config().messages(); if (target == null) { Text.send(ctx.sender(), msg.playerNotFound()); return; @@ -553,15 +477,15 @@ private void onAdminImport(@NonNull CommandContext ctx, @NonNull Argument tar private void onAlerts(@NonNull CommandContext ctx) { if (!ctx.isPlayer()) { - Text.send(ctx.sender(), manager.configManager().get().messages().playerOnly()); + Text.send(ctx.sender(), manager.config().messages().playerOnly()); return; } Player player = ctx.playerOrThrow(); boolean enabled = manager.toggleAlerts(player.getUniqueId()); if (enabled) { - Text.send(player, manager.configManager().get().messages().alertsEnabled()); + Text.send(player, manager.config().messages().alertsEnabled()); } else { - Text.send(player, manager.configManager().get().messages().alertsDisabled()); + Text.send(player, manager.config().messages().alertsDisabled()); } } @@ -570,9 +494,26 @@ private void onDebug(@NonNull CommandContext ctx) { OumLib.setDebug(!current); boolean enabled = !current; if (enabled) { - Text.send(ctx.sender(), manager.configManager().get().messages().debugEnabled()); + Text.send(ctx.sender(), manager.config().messages().debugEnabled()); } else { - Text.send(ctx.sender(), manager.configManager().get().messages().debugDisabled()); + Text.send(ctx.sender(), manager.config().messages().debugDisabled()); } } + + private void onAdminPrune(@NonNull CommandContext ctx) { + var msg = manager.config().messages(); + int days = ctx.args().getInt("days"); + if (days <= 0) { + Text.send(ctx.sender(), msg.invalidDays()); + return; + } + + manager.pruneInactiveProfiles(days).thenAccept(count -> { + if (count > 0) { + Text.send(ctx.sender(), msg.adminPruneSuccess(), "count", String.valueOf(count), "days", String.valueOf(days)); + } else { + Text.send(ctx.sender(), msg.adminPruneNone(), "days", String.valueOf(days)); + } + }); + } } \ No newline at end of file diff --git a/src/main/java/dev/oum/profile/config/MainConfig.java b/src/main/java/dev/oum/profile/config/MainConfig.java new file mode 100644 index 0000000..42c4675 --- /dev/null +++ b/src/main/java/dev/oum/profile/config/MainConfig.java @@ -0,0 +1,184 @@ +package dev.oum.profile.config; + +import dev.oum.oumlib.config.Comment; +import dev.oum.oumlib.config.ConfigSection; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.List; + +@Comment({ + " ___ ___ __ _ _ ", + " ╱___╲_ _ _ __ ___ ╱ _ ╲_ __ ___ ╱ _(_) │ ___ ", + " ╱╱ ╱╱ │ │ │ '_ ` _ ╲ ╱ ╱_)╱ '__╱ _ ╲│ │_│ │ │╱ _ ╲", + "╱ ╲_╱╱│ │_│ │ │ │ │ │ ╱ ___╱│ │ (_) │ _│ │ │ __╱", + "╲___╱ ╲__,_│_│ │_│ │_╲╱ │_│ ╲___╱│_│ │_│_│╲___│", + "", + "OumProfile Main Configuration", + "Configure core mechanics, database storage, profile switching, and integrations.", + "Menus are configured in 'menus.yml' and messages in 'messages.yml'." +}) +public record MainConfig( + @Comment("Enable detailed debug logging in console") + boolean debug, + + @Comment("Name of the default profile created on first join") + String defaultProfileName, + + @Comment("Global date format pattern") + String dateFormat, + + @Comment("Enable administrative alerts when players switch, create, or delete profiles") + boolean adminAlertsEnabled, + + @Comment("Maximum character length for profile names") + int profileNameMaxLength, + + @Comment("Regex pattern for valid profile names") + String profileNameRegex, + + @Comment("Max profile limits based on profiles.max. permission nodes") + List limitTiers, + + @Comment("Periodic background auto-save settings for active player profiles") + AutoSaveSection autoSave, + + @Comment("Database storage settings (SQLite/MySQL)") + StorageSection storage, + + @Comment("Profile switching mechanics, warmups, combat checks, and sounds") + SwitchSection switching, + + @Comment("LuckPerms rank synchronization") + LuckPermsSection luckperms, + + @Comment("Multi-currency economy settings") + EconomySection economy, + + @Comment("Skill and Job synchronization settings") + SkillSection skills, + + @Comment("Vanilla statistics synchronization settings") + StatisticsSection statistics +) implements ConfigSection { + + @Contract(" -> new") + public static @NonNull MainConfig defaults() { + return new MainConfig( + false, + "default", + "yyyy-MM-dd HH:mm", + true, + 16, + "[a-zA-Z0-9_-]+", + List.of(1, 3, 5, 10), + new AutoSaveSection(true, 5), + new StorageSection("sqlite", "localhost", 3306, "oumprofile", "root", ""), + new SwitchSection( + true, 5, true, true, true, 10, 10, false, + true, "Switching Profile...", "Do not move for s", + true, "block.note_block.hat", "entity.player.levelup", "entity.villager.no" + ), + new LuckPermsSection(true), + new EconomySection(true, List.of("vault", "playerpoints")), + new SkillSection(true, true, true), + new StatisticsSection(true, List.of("MOB_KILLS", "DEATHS", "JUMP")) + ); + } + + public record AutoSaveSection( + @Comment("Enable periodic background auto-save for online players") + boolean enabled, + + @Comment("Auto-save interval in minutes (e.g. 5 for every 5 minutes)") + int intervalMinutes + ) implements ConfigSection { + @Contract(" -> new") + public static @NonNull AutoSaveSection defaults() { + return new AutoSaveSection(true, 5); + } + } + + public record StorageSection( + @Comment("Database type: 'sqlite' or 'mysql'") + String type, + @Comment("MySQL database hostname") + String host, + @Comment("MySQL port number") + int port, + @Comment("Database schema name") + String database, + @Comment("MySQL username") + String username, + @Comment("MySQL password") + String password + ) implements ConfigSection { + } + + public record SwitchSection( + @Comment("Enable countdown warmup duration when switching profiles") + boolean warmupEnabled, + @Comment("Warmup duration in seconds") + int warmupSeconds, + @Comment("Cancel warmups if the player moves") + boolean cancelOnMove, + @Comment("Cancel warmups if the player receives damage") + boolean cancelOnDamage, + @Comment("Cancel warmups if the player is in combat") + boolean cancelInCombat, + @Comment("Combat tag duration in seconds") + int combatTagDuration, + @Comment("Cooldown time in seconds before switching profiles again") + int switchCooldownSeconds, + @Comment("Save and restore coordinates/location per profile") + boolean saveLocation, + + @Comment("Send title countdowns during profile switching") + boolean warmupTitleEnabled, + @Comment("Countdown title format (MiniMessage support, placeholder )") + String warmupTitleText, + @Comment("Countdown subtitle format (MiniMessage support, placeholder )") + String warmupSubtitleText, + @Comment("Play sound on each tick of the warmup countdown") + boolean warmupSoundEnabled, + @Comment("Warmup tick sound key") + String warmupSoundKey, + @Comment("Warmup completion sound key") + String warmupCompleteSoundKey, + @Comment("Warmup cancellation sound key") + String warmupCancelSoundKey + ) implements ConfigSection { + } + + public record LuckPermsSection( + @Comment("Synchronize permission groups using LuckPerms integration") + boolean enabled + ) implements ConfigSection { + } + + public record EconomySection( + @Comment("Enable multi-currency economy storage") + boolean enabled, + @Comment("List of currencies to save and restore per-profile (e.g. vault, playerpoints)") + List currencies + ) implements ConfigSection { + } + + public record SkillSection( + @Comment("Enable mcMMO skill level synchronization") + boolean mcmmoEnabled, + @Comment("Enable AuraSkills/AureliumSkills level synchronization") + boolean auraSkillsEnabled, + @Comment("Enable JobsReborn job level synchronization") + boolean jobsEnabled + ) implements ConfigSection { + } + + public record StatisticsSection( + @Comment("Enable vanilla statistics synchronization") + boolean enabled, + @Comment("List of statistic names to save and restore per-profile") + List tracked + ) implements ConfigSection { + } +} diff --git a/src/main/java/dev/oum/profile/config/MenusConfig.java b/src/main/java/dev/oum/profile/config/MenusConfig.java new file mode 100644 index 0000000..2f120f2 --- /dev/null +++ b/src/main/java/dev/oum/profile/config/MenusConfig.java @@ -0,0 +1,260 @@ +package dev.oum.profile.config; + +import dev.oum.oumlib.config.Comment; +import dev.oum.oumlib.config.ConfigSection; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.List; + +@Comment({ + "OumProfile Menus Configuration", + "Customize inventory GUI titles, dimensions, patterns, buttons, and confirmation dialogs." +}) +public record MenusConfig( + @Comment("Main profile selection GUI settings") + GuiSection gui, + + @Comment("Confirmation GUI settings for deleting profiles") + ConfirmGuiSection confirmDelete, + + @Comment("Confirmation GUI settings for creating profiles") + ConfirmGuiSection confirmCreate +) implements ConfigSection { + + @Contract(" -> new") + public static @NonNull MenusConfig defaults() { + return new MenusConfig( + new GuiSection( + "Select a Profile", + 3, + List.of( + "#########", + " PPPPP ", + "####C####" + ), + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWZmMzE0MzFkNjQ1ODdmZjZlZjk4YzA2NzU4MTA2ODFmOGMxM2JmOTZmNTFkOWNiMDdlZDc4NTJiMmZmZDEifX19", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODE5OWI1ZWUzMjBlNzk5N2Q5MWJiNWY4NjY1ZjNkMzJhZTQ5MjBlMDNjNmIzZDliN2VlY2E2OTcxMTk5OTcifX19", + "Create New Profile", + "Profile Limit Reached", + List.of( + "Slots: / ", + "", + "Click to start profile creation" + ), + List.of( + "Slots: / ", + "", + "Purchase more slots on our store" + ), + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjdmYWFlMWQxOTgzNmJkMDc4NTQyNmU0ZmQyOGFhNjNhMzgxZTllNzE0OTU1OWVlNmIyYTUwOTk5NWJiY2ZkMiJ9fX0=", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDVjNmRjMmJiZjUxYzM2Y2ZjNzcxNDU4NWE2YTU2ODNlZjJiMTRkNDdkOGZmNzE0NjU0YTg5M2Y1ZGE2MjIifX19", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDZiYTYzMzQ0ZjQ5ZGQxYzRmNTQ4OGU5MjZiZjNkOWUyYjI5OTE2YTZjNTBkNjEwYmI0MGE1MjczZGM4YzgyIn19fQ==", + " (Active)", + "", + "Empty Slot", + List.of( + "━━━━━━━━━━━━━━━━━━━━━", + "Created: ", + "Last Used: ", + "Playtime: ", + "Balance: $", + "Rank Group: ", + "Jobs: ", + "━━━━━━━━━━━━━━━━━━━━━", + "Currently Active" + ), + List.of( + "━━━━━━━━━━━━━━━━━━━━━", + "Created: ", + "Last Used: ", + "Playtime: ", + "Balance: $", + "Rank Group: ", + "Jobs: ", + "━━━━━━━━━━━━━━━━━━━━━", + "Left-Click to switch", + "Right-Click to delete" + ), + List.of( + "━━━━━━━━━━━━━━━━━━━━━", + "Click on the Create button", + "below to start a new profile.", + "━━━━━━━━━━━━━━━━━━━━━" + ), + "GRAY_STAINED_GLASS_PANE", + " ", + "P", + "C", + "Type a profile name in chat:", + 15, + "cancel", + true, + "block.chest.open", + true, + "ui.button.click", + true, + "block.chest.close", + true, + "entity.villager.no" + ), + new ConfirmGuiSection( + true, + "Confirm Deleting ", + 3, + List.of( + "#########", + " C D ", + "#########" + ), + "C", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==", + "Confirm Deletion", + List.of( + "Clicking here will permanently", + "delete the profile .", + "", + "WARNING: This cannot be undone!" + ), + "D", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=", + "Cancel", + List.of( + "Click to keep your profile", + "and return to the menu." + ), + "GRAY_STAINED_GLASS_PANE", + " " + ), + new ConfirmGuiSection( + true, + "Confirm Creating ", + 3, + List.of( + "#########", + " C D ", + "#########" + ), + "C", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=", + "Confirm Creation", + List.of( + "Click here to create", + "profile ." + ), + "D", + "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==", + "Cancel", + List.of( + "Click to cancel creation", + "and return to the menu." + ), + "GRAY_STAINED_GLASS_PANE", + " " + ) + ); + } + + public record GuiSection( + @Comment("Title of the profile inventory GUI") + String title, + @Comment("Number of rows in the profile GUI") + int rows, + @Comment("Inventory pattern structure") + List pattern, + @Comment("Material for the creation button") + String createButtonMaterial, + @Comment("Material for creation button when limits are reached") + String createButtonMaterialLimitReached, + @Comment("Display name for the create profile button") + String createButtonName, + @Comment("Display name for create button when limits are reached") + String createButtonNameLimitReached, + @Comment("Lore lines for the create profile button") + List createButtonLore, + @Comment("Lore lines for create button when limits are reached") + List createButtonLoreLimitReached, + @Comment("Material for the active profile button") + String activeProfileMaterial, + @Comment("Material for inactive profile buttons") + String inactiveProfileMaterial, + @Comment("Material for empty profile slots") + String emptySlotMaterial, + @Comment("Display name for the active profile button") + String activeProfileName, + @Comment("Display name for inactive profile buttons") + String inactiveProfileName, + @Comment("Display name for empty slots") + String emptySlotName, + @Comment("Lore lines for the active profile button") + List activeProfileLore, + @Comment("Lore lines for inactive profile buttons") + List inactiveProfileLore, + @Comment("Lore lines for empty profile slots") + List emptySlotLore, + @Comment("Material for the GUI border filler items") + String borderMaterial, + @Comment("Display name for the GUI border items") + String borderName, + @Comment("Character key representing profile slots in the pattern") + String profileSlotChar, + @Comment("Character key representing create button in the pattern") + String createButtonSlotChar, + @Comment("Prompt message sent in chat when naming a new profile") + String promptMessage, + @Comment("Time limit in seconds to enter a profile name in chat") + int textInputTimeoutSeconds, + @Comment("Word players can type in chat to cancel profile creation") + String cancelWord, + + @Comment("Play sound when opening profile GUI") + boolean openSoundEnabled, + @Comment("Sound key when opening GUI") + String openSoundKey, + @Comment("Play sound on profile selection click") + boolean clickSoundEnabled, + @Comment("Sound key for selection click") + String clickSoundKey, + @Comment("Play sound when closing GUI") + boolean closeSoundEnabled, + @Comment("Sound key when closing GUI") + String closeSoundKey, + @Comment("Play sound on invalid actions or errors in GUI") + boolean errorSoundEnabled, + @Comment("Sound key for GUI error feedback") + String errorSoundKey + ) implements ConfigSection { + } + + public record ConfirmGuiSection( + @Comment("Enable confirmation menu") + boolean enabled, + @Comment("Title of the confirmation menu") + String title, + @Comment("Number of rows in the menu") + int rows, + @Comment("Pattern defining layout slots") + List pattern, + @Comment("Slot character for the Confirm button") + String confirmSlotChar, + @Comment("Item material/head for Confirm button") + String confirmMaterial, + @Comment("Display name for Confirm button") + String confirmName, + @Comment("Lore lines for Confirm button") + List confirmLore, + @Comment("Slot character for the Deny/Cancel button") + String denySlotChar, + @Comment("Item material/head for Deny/Cancel button") + String denyMaterial, + @Comment("Display name for Deny/Cancel button") + String denyName, + @Comment("Lore lines for Deny/Cancel button") + List denyLore, + @Comment("Material for menu border filler items") + String borderMaterial, + @Comment("Display name for border items") + String borderName + ) implements ConfigSection { + } +} diff --git a/src/main/java/dev/oum/profile/config/MessagesConfig.java b/src/main/java/dev/oum/profile/config/MessagesConfig.java new file mode 100644 index 0000000..60478aa --- /dev/null +++ b/src/main/java/dev/oum/profile/config/MessagesConfig.java @@ -0,0 +1,139 @@ +package dev.oum.profile.config; + +import dev.oum.oumlib.config.Comment; +import dev.oum.oumlib.config.ConfigSection; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +@Comment({ + "OumProfile Messages Configuration", + "Customize all user-facing chat messages, alerts, and notifications.", + "MiniMessage gradient, color, and formatting tags are fully supported." +}) +public record MessagesConfig( + String profileNotFound, + String profileAlreadyActive, + String combatBlock, + String warmupStart, + String switchSuccess, + String noPermission, + String createFail, + String createSuccess, + String deleteFail, + String deleteSuccess, + String help, + String listHeader, + String listItemActive, + String listItemInactive, + String currentProfile, + String playerOnly, + String noProfiles, + String noActiveProfile, + String reloadSuccess, + String switchCooldown, + String maxProfilesReached, + String cannotDeleteActive, + String cannotDeleteDefault, + String invalidProfileName, + String profileCreationCancelled, + String profileCreationTimedOut, + String playerNotFound, + String adminOpenSuccess, + String adminListHeader, + String adminCreateSuccess, + String adminCreateFail, + String adminSwitchSuccess, + String adminSwitchFailNoProfile, + String adminDeleteSuccess, + String adminDeleteFail, + String adminAlertSwitch, + String adminAlertCreate, + String adminAlertDelete, + String alertsEnabled, + String alertsDisabled, + String warmupCancelledMove, + String warmupCancelledDamage, + String warmupCancelledGeneric, + String debugEnabled, + String debugDisabled, + String renameSuccess, + String renameFail, + String cannotRenameDefault, + String adminRenameSuccess, + String adminRenameFail, + String adminAlertRename, + String profileNameTooLong, + String profileNameInvalidChars, + String exportSuccess, + String importSuccess, + String importFail, + String adminPruneSuccess, + String adminPruneNone, + String invalidDays +) implements ConfigSection { + + @Contract(" -> new") + public static @NonNull MessagesConfig defaults() { + return new MessagesConfig( + "Profile '' does not exist.", + "You are already using that profile.", + "You cannot switch profiles while in combat.", + "Switching to in s...", + "Switched to profile .", + "You don't have permission to create a profile named ''.", + "Could not create profile '' (limit reached or name exists).", + "Created profile .", + "Could not delete profile '' (active, last remaining, or not found).", + "Deleted profile .", + "OumProfile » /profile ", + "Your profiles ():", + " Active", + " Last used ", + "Active Profile: ", + "This command can only be executed by players.", + "You do not have any profiles yet.", + "You do not have an active profile loaded.", + "Configuration reloaded successfully.", + "You must wait before switching profiles again.", + "You have reached the maximum number of profiles allowed ().", + "You cannot delete your active profile. Switch to another profile first.", + "You cannot delete the default profile.", + "Profile name cannot be empty or contain spaces.", + "Profile creation cancelled.", + "Profile creation timed out.", + "Player not found.", + "Opened profile menu for .", + "Profiles for ():", + "Created profile for .", + "Failed to create profile for player (limit reached or name exists).", + "Switched to profile .", + "Player does not have profile ''.", + "Deleted profile for .", + "Failed to delete profile for player (active, last profile, or not found).", + "ALERT | switched to profile ", + "ALERT | created profile ", + "ALERT | deleted profile ", + "Profile alerts enabled.", + "Profile alerts disabled.", + "Profile switch cancelled because you moved.", + "Profile switch cancelled because you took damage.", + "Profile switch cancelled.", + "Debug mode enabled.", + "Debug mode disabled.", + "Renamed profile to .", + "Could not rename profile ''.", + "You cannot rename the default profile.", + "Renamed profile to for .", + "Failed to rename profile for player.", + "ALERT | renamed profile to ", + "Profile name must be at most characters.", + "Profile name contains invalid characters. Only letters, numbers, hyphens and underscores are allowed.", + "Exported profile to file.", + "Imported profile for .", + "Failed to import profile from file.", + "Successfully pruned inactive profile(s) older than days.", + "No inactive profiles older than days were found to prune.", + "Please specify a valid number of days (greater than 0)." + ); + } +} diff --git a/src/main/java/dev/oum/profile/config/ProfileConfig.java b/src/main/java/dev/oum/profile/config/ProfileConfig.java index 27fc985..991b686 100644 --- a/src/main/java/dev/oum/profile/config/ProfileConfig.java +++ b/src/main/java/dev/oum/profile/config/ProfileConfig.java @@ -1,544 +1,55 @@ package dev.oum.profile.config; -import dev.oum.oumlib.config.Comment; -import dev.oum.oumlib.config.ConfigSection; -import org.jetbrains.annotations.Contract; +import dev.oum.oumlib.config.ConfigManager; import org.jspecify.annotations.NonNull; -import java.util.List; +public final class ProfileConfig { -@Comment({ - " ___ ___ __ _ _ ", - " ╱___╲_ _ _ __ ___ ╱ _ ╲_ __ ___ ╱ _(_) │ ___ ", - " ╱╱ ╱╱ │ │ │ '_ ` _ ╲ ╱ ╱_)╱ '__╱ _ ╲│ │_│ │ │╱ _ ╲", - "╱ ╲_╱╱│ │_│ │ │ │ │ │ ╱ ___╱│ │ (_) │ _│ │ │ __╱", - "╲___╱ ╲__,_│_│ │_│ │_╲╱ │_│ ╲___╱│_│ │_│_│╲___│", - "", - "Welcome to OumProfile configuration!", - "Below you can customize how player profiles behave on your server.", - "Made by sun-plugins", - "", - "==================== QUICK SETUP GUIDE ====================", - "", - "1. DATABASE TYPE (storage.type):", - " - Use 'sqlite' for single-server setups (creates a local file in your plugin folder).", - " - Use 'mysql' if you are syncing profiles across multiple servers (requires a shared MySQL server).", - "", - "2. PROFILE SLOTS & LIMITS:", - " - By default, players can only have 1 profile.", - " - Grant players 'profiles.max.' (e.g., profiles.max.3) to allow more profile slots.", - " - You can define slots tiers (like 1, 3, 5, 10) in the 'limit-tiers' list below.", - " - Give players 'profiles.max.unlimited' to bypass all slot limits.", - "", - "3. INTEGRATIONS:", - " - LuckPerms (luckperms.enabled): When active, switching profiles restores the player's saved rank group.", - " - Vault (automatically integrated): When active, each profile keeps its own separate money balance.", - "", - "4. ANTI-EXPLOIT (switching.cancel-on-move, cancel-in-combat):", - " - Keeps players from escaping PvP. If they are in combat or move during the switch warmup,", - " the switch gets cancelled.", - "", - "===========================================================", - "", - "Need help or found a bug? Join our Discord: https://discord.gg/maDcwPV6KB", - "Or report on GitHub: https://github.com/sun-mc-dev/oumprofile/issues" -}) -public record ProfileConfig( - @Comment("Enable detailed debug logging in console") - boolean debug, + private final ConfigManager main; + private final ConfigManager menus; + private final ConfigManager messages; - @Comment("Profile switching settings") - SwitchSection switching, - - @Comment("Database storage settings (SQLite/MySQL)") - StorageSection storage, - - @Comment("LuckPerms group integration") - LuckPermsSection luckperms, - - @Comment("Custom plugin messages (MiniMessage tags allowed)") - MessagesSection messages, - - @Comment("GUI menus and chat input settings") - GuiSection gui, - - @Comment("Confirmation GUI settings for deleting profiles") - ConfirmGuiSection confirmDelete, - - @Comment("Confirmation GUI settings for creating profiles") - ConfirmGuiSection confirmCreate, - - @Comment("Max profile limits based on oumprofile.max. permission nodes") - List limitTiers, - - @Comment("Name of the default profile created on first join") - String defaultProfileName, - - @Comment("Global date format pattern") - String dateFormat, - - @Comment("Enable administrative alerts when players switch, create, or delete profiles") - boolean adminAlertsEnabled, - - @Comment("Multi-currency economy settings") - EconomySection economy, - - @Comment("Skill and Job synchronization settings") - SkillSection skills, - - @Comment("Vanilla statistics synchronization settings") - StatisticsSection statistics, - - @Comment("Maximum character length for profile names") - int profileNameMaxLength, - - @Comment("Regex pattern for valid profile names") - String profileNameRegex -) implements ConfigSection { - - @Contract(" -> new") - public static @NonNull ProfileConfig defaults() { - return new ProfileConfig( - false, - new SwitchSection( - true, 5, true, true, true, 10, 10, false, - true, "Switching Profile...", "Do not move for s", - true, "block.note_block.hat", "entity.player.levelup", "entity.villager.no" - ), - new StorageSection("sqlite", "localhost", 3306, "oumprofile", "root", ""), - new LuckPermsSection(true), - new MessagesSection( - "Profile '' does not exist.", - "You are already using that profile.", - "You cannot switch profiles while in combat.", - "Switching to in s...", - "Switched to profile .", - "You don't have permission to create a profile named ''.", - "Could not create profile '' (limit reached or name exists).", - "Created profile .", - "Could not delete profile '' (active, last remaining, or not found).", - "Deleted profile .", - "OumProfile » /profile ", - "Your profiles ():", - " Active", - " Last used ", - "Active Profile: ", - "This command must be run as a player.", - "You have no profiles.", - "No active profile found.", - "Configuration reloaded successfully.", - "Please wait s before switching profiles again.", - "You have reached your maximum profile slot limit.", - "You cannot delete your active profile.", - "You cannot delete your default profile.", - "Profile name must not be empty or contain spaces.", - "Profile creation cancelled.", - "Profile creation timed out.", - "Player not found.", - "Opened profile menu for .", - "Profiles for ():", - "Successfully created profile for .", - "Failed to create profile (already exists or limit reached).", - "Forced to switch to profile .", - "Player does not have a profile named ''.", - "Successfully deleted profile for .", - "Failed to delete profile (active, last remaining, or not found).", - "ALERT | switched to profile ", - "ALERT | created profile ", - "ALERT | deleted profile ", - "Profile alerts enabled.", - "Profile alerts disabled.", - "Profile switch cancelled because you moved.", - "Profile switch cancelled because you took damage.", - "Profile switch cancelled.", - "Debug mode enabled.", - "Debug mode disabled.", - "Renamed profile to .", - "Could not rename profile ''.", - "You cannot rename the default profile.", - "Renamed profile to for .", - "Failed to rename profile for player.", - "ALERT | renamed profile to ", - "Profile name must be at most characters.", - "Profile name contains invalid characters. Only letters, numbers, hyphens and underscores are allowed.", - "Exported profile to file.", - "Imported profile for .", - "Failed to import profile from file." - ), - new GuiSection( - "Select a Profile", - 3, - List.of( - "#########", - " PPPPP ", - "####C####" - ), - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWZmMzE0MzFkNjQ1ODdmZjZlZjk4YzA2NzU4MTA2ODFmOGMxM2JmOTZmNTFkOWNiMDdlZDc4NTJiMmZmZDEifX19", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODE5OWI1ZWUzMjBlNzk5N2Q5MWJiNWY4NjY1ZjNkMzJhZTQ5MjBlMDNjNmIzZDliN2VlY2E2OTcxMTk5OTcifX19", - "Create New Profile", - "Profile Limit Reached", - List.of( - "Slots: / ", - "", - "Click to start profile creation" - ), - List.of( - "Slots: / ", - "", - "Purchase more slots on our store" - ), - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjdmYWFlMWQxOTgzNmJkMDc4NTQyNmU0ZmQyOGFhNjNhMzgxZTllNzE0OTU1OWVlNmIyYTUwOTk5NWJiY2ZkMiJ9fX0=", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDVjNmRjMmJiZjUxYzM2Y2ZjNzcxNDU4NWE2YTU2ODNlZjJiMTRkNDdkOGZmNzE0NjU0YTg5M2Y1ZGE2MjIifX19", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDZiYTYzMzQ0ZjQ5ZGQxYzRmNTQ4OGU5MjZiZjNkOWUyYjI5OTE2YTZjNTBkNjEwYmI0MGE1MjczZGM4YzgyIn19fQ==", - " (Active)", - "", - "Empty Slot", - List.of( - "━━━━━━━━━━━━━━━━━━━━━", - "Created: ", - "Last Used: ", - "Playtime: ", - "Balance: $", - "Rank Group: ", - "Jobs: ", - "━━━━━━━━━━━━━━━━━━━━━", - "Currently Active" - ), - List.of( - "━━━━━━━━━━━━━━━━━━━━━", - "Created: ", - "Last Used: ", - "Playtime: ", - "Balance: $", - "Rank Group: ", - "Jobs: ", - "━━━━━━━━━━━━━━━━━━━━━", - "Left-Click to switch", - "Right-Click to delete" - ), - "GRAY_STAINED_GLASS_PANE", - " ", - "Profile Creation\nType a name in chat for your new profile.\nType cancel to return.", - "cancel", - 30, - "P", - "C", - true, - "block.chest.open", - true, - "ui.button.click", - true, - "entity.villager.no", - true, - "block.chest.close" - ), - new ConfirmGuiSection( - true, - "Confirm Deleting ", - 3, - List.of( - "#########", - " C D ", - "#########" - ), - "C", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==", - "Confirm Deletion", - List.of( - "Clicking here will permanently", - "delete the profile .", - "", - "WARNING: This cannot be undone!" - ), - "D", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=", - "Cancel", - List.of( - "Click to keep your profile", - "and return to the menu." - ), - "GRAY_STAINED_GLASS_PANE", - " " - ), - new ConfirmGuiSection( - true, - "Confirm Creating ", - 3, - List.of( - "#########", - " C D ", - "#########" - ), - "C", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDMxMmNhNDYzMmRlZjVmZmFmMmViMGQ5ZDdjYzdiNTVhNTBjNGUzOTIwZDkwMzcyYWFiMTQwNzgxZjVkZmJjNCJ9fX0=", - "Confirm Creation", - List.of( - "Click here to create", - "profile ." - ), - "D", - "head:eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmViNTg4YjIxYTZmOThhZDFmZjRlMDg1YzU1MmRjYjA1MGVmYzljYWI0MjdmNDYwNDhmMThmYzgwMzQ3NWY3In19fQ==", - "Cancel", - List.of( - "Click to cancel creation", - "and return to the menu." - ), - "GRAY_STAINED_GLASS_PANE", - " " - ), - List.of(1, 3, 5, 10), - "default", - "yyyy-MM-dd HH:mm", - true, - new EconomySection(true, List.of("vault", "playerpoints")), - new SkillSection(true, true, true), - new StatisticsSection(true, List.of("MOB_KILLS", "DEATHS", "JUMP")), - 16, - "[a-zA-Z0-9_-]+" - ); + public ProfileConfig( + @NonNull ConfigManager main, + @NonNull ConfigManager menus, + @NonNull ConfigManager messages + ) { + this.main = main; + this.menus = menus; + this.messages = messages; } - public record EconomySection( - @Comment("Enable multi-currency economy storage") - boolean enabled, - @Comment("List of currencies to save and restore per-profile (e.g. vault, playerpoints)") - List currencies - ) implements ConfigSection { - } + public static @NonNull ProfileConfig create(Runnable onReloadAction) { + ConfigManager main = ConfigManager.of(MainConfig.class, "config.yml", MainConfig::defaults) + .onReload(cfg -> { + if (onReloadAction != null) onReloadAction.run(); + }) + .enableAutoReload(); - public record SkillSection( - @Comment("Enable mcMMO skill level synchronization") - boolean mcmmoEnabled, - @Comment("Enable AuraSkills/AureliumSkills level synchronization") - boolean auraSkillsEnabled, - @Comment("Enable JobsReborn job level synchronization") - boolean jobsEnabled - ) implements ConfigSection { - } + ConfigManager menus = ConfigManager.of(MenusConfig.class, "menus.yml", MenusConfig::defaults) + .enableAutoReload(); - public record StatisticsSection( - @Comment("Enable vanilla statistics synchronization") - boolean enabled, - @Comment("List of statistic names to save and restore per-profile") - List tracked - ) implements ConfigSection { - } - - public record SwitchSection( - @Comment("Enable countdown warmup duration when switching profiles") - boolean warmupEnabled, - @Comment("Warmup duration in seconds") - int warmupSeconds, - @Comment("Cancel warmups if the player moves") - boolean cancelOnMove, - @Comment("Cancel warmups if the player receives damage") - boolean cancelOnDamage, - @Comment("Cancel warmups if the player is in combat") - boolean cancelInCombat, - @Comment("Combat tag duration in seconds") - int combatTagDuration, - @Comment("Cooldown time in seconds before switching profiles again") - int switchCooldownSeconds, - @Comment("Save and restore coordinates/location per profile") - boolean saveLocation, + ConfigManager messages = ConfigManager.of(MessagesConfig.class, "messages.yml", MessagesConfig::defaults) + .enableAutoReload(); - @Comment("Send title countdowns during profile switching") - boolean warmupTitleEnabled, - @Comment("Countdown title format (MiniMessage support, placeholder )") - String warmupTitleText, - @Comment("Countdown subtitle format (MiniMessage support, placeholder )") - String warmupSubtitleText, - @Comment("Play sound on each tick of the warmup countdown") - boolean warmupSoundEnabled, - @Comment("Warmup tick sound key") - String warmupSoundKey, - @Comment("Warmup completion sound key") - String warmupCompleteSoundKey, - @Comment("Warmup cancellation sound key") - String warmupCancelSoundKey - ) implements ConfigSection { + return new ProfileConfig(main, menus, messages); } - public record StorageSection( - @Comment("Database type: 'sqlite' or 'mysql'") - String type, - @Comment("MySQL database hostname") - String host, - @Comment("MySQL port number") - int port, - @Comment("Database schema name") - String database, - @Comment("MySQL username") - String username, - @Comment("MySQL password") - String password - ) implements ConfigSection { + public @NonNull MainConfig main() { + return main.get(); } - public record LuckPermsSection( - @Comment("Synchronize permission groups using LuckPerms integration") - boolean enabled - ) implements ConfigSection { + public @NonNull MenusConfig menus() { + return menus.get(); } - public record MessagesSection( - String profileNotFound, - String profileAlreadyActive, - String combatBlock, - String warmupStart, - String switchSuccess, - String noPermission, - String createFail, - String createSuccess, - String deleteFail, - String deleteSuccess, - String help, - String listHeader, - String listItemActive, - String listItemInactive, - String currentProfile, - String playerOnly, - String noProfiles, - String noActiveProfile, - String reloadSuccess, - String switchCooldown, - String maxProfilesReached, - String cannotDeleteActive, - String cannotDeleteDefault, - String invalidProfileName, - String profileCreationCancelled, - String profileCreationTimedOut, - String playerNotFound, - String adminOpenSuccess, - String adminListHeader, - String adminCreateSuccess, - String adminCreateFail, - String adminSwitchSuccess, - String adminSwitchFailNoProfile, - String adminDeleteSuccess, - String adminDeleteFail, - String adminAlertSwitch, - String adminAlertCreate, - String adminAlertDelete, - String alertsEnabled, - String alertsDisabled, - String warmupCancelledMove, - String warmupCancelledDamage, - String warmupCancelledGeneric, - String debugEnabled, - String debugDisabled, - String renameSuccess, - String renameFail, - String cannotRenameDefault, - String adminRenameSuccess, - String adminRenameFail, - String adminAlertRename, - String profileNameTooLong, - String profileNameInvalidChars, - String exportSuccess, - String importSuccess, - String importFail - ) implements ConfigSection { - } - - public record GuiSection( - @Comment("Title of the profile inventory GUI") - String title, - @Comment("Number of rows in the profile GUI") - int rows, - @Comment("Inventory pattern structure") - List pattern, - @Comment("Material for the creation button") - String createButtonMaterial, - @Comment("Material for creation button when limits are reached") - String createButtonMaterialLimitReached, - @Comment("Name of the creation button") - String createButtonName, - @Comment("Name of creation button when limits are reached") - String createButtonNameLimitReached, - @Comment("Lore of the creation button") - List createButtonLore, - @Comment("Lore of creation button when limits are reached") - List createButtonLoreLimitReached, - @Comment("Material for the currently active profile item") - String activeProfileMaterial, - @Comment("Material for inactive profile items") - String inactiveProfileMaterial, - @Comment("Material for empty slot profile items") - String emptySlotMaterial, - @Comment("DisplayName pattern for the active profile") - String activeProfileName, - @Comment("DisplayName pattern for inactive profiles") - String inactiveProfileName, - @Comment("DisplayName pattern for empty slots") - String emptySlotName, - @Comment("Lore layout for the active profile item") - List activeProfileLore, - @Comment("Lore layout for inactive profile items") - List inactiveProfileLore, - @Comment("Material used for border glass items") - String borderMaterial, - @Comment("DisplayName of border glass items") - String borderName, - @Comment("Prompt printed in chat when player starts creation input") - String promptMessage, - @Comment("Keyword player types to abort creation") - String cancelWord, - @Comment("How long in seconds until chat input times out") - int textInputTimeoutSeconds, - @Comment("Layout character representing profile items") - String profileSlotChar, - @Comment("Layout character representing the creation button") - String createButtonSlotChar, - - @Comment("Play sound when GUI is opened") - boolean openSoundEnabled, - @Comment("GUI open sound key") - String openSoundKey, - @Comment("Play sound when clicking GUI items") - boolean clickSoundEnabled, - @Comment("GUI click sound key") - String clickSoundKey, - @Comment("Play sound on errors/limits in GUI") - boolean errorSoundEnabled, - @Comment("GUI error sound key") - String errorSoundKey, - @Comment("Play sound when GUI is closed") - boolean closeSoundEnabled, - @Comment("GUI close sound key") - String closeSoundKey - ) implements ConfigSection { + public @NonNull MessagesConfig messages() { + return messages.get(); } - public record ConfirmGuiSection( - @Comment("Enable confirmation GUI") - boolean enabled, - @Comment("Title of the confirmation GUI") - String title, - @Comment("Number of rows in the GUI") - int rows, - @Comment("Inventory pattern structure") - List pattern, - @Comment("Confirm button character in pattern") - String confirmSlotChar, - @Comment("Confirm button material") - String confirmMaterial, - @Comment("Confirm button display name") - String confirmName, - @Comment("Confirm button lore") - List confirmLore, - @Comment("Deny button character in pattern") - String denySlotChar, - @Comment("Deny button material") - String denyMaterial, - @Comment("Deny button display name") - String denyName, - @Comment("Deny button lore") - List denyLore, - @Comment("Material used for border glass items") - String borderMaterial, - @Comment("DisplayName of border glass items") - String borderName - ) implements ConfigSection { + public void reload() { + main.reload(); + menus.reload(); + messages.reload(); } } \ No newline at end of file diff --git a/src/main/java/dev/oum/profile/config/ProfileStorage.java b/src/main/java/dev/oum/profile/config/ProfileStorage.java index 54ecbf3..cf51978 100644 --- a/src/main/java/dev/oum/profile/config/ProfileStorage.java +++ b/src/main/java/dev/oum/profile/config/ProfileStorage.java @@ -10,29 +10,44 @@ import java.io.File; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.UUID; public final class ProfileStorage { private final Database db; private final boolean mysql; + private final String saveSql; - public ProfileStorage(ProfileConfig.@NonNull StorageSection cfg) { + public ProfileStorage(MainConfig.@NonNull StorageSection cfg) { this.mysql = cfg.type().equalsIgnoreCase("mysql"); if (mysql) { this.db = Database.mysql(cfg.host(), cfg.port(), cfg.database(), cfg.username(), cfg.password()); db.runMigrations(ProfileStorage.class, "migrations/mysql/V1__init.sql", "migrations/mysql/V2__add_active_column.sql"); + this.saveSql = "INSERT INTO oum_profiles (uuid, name, created_at, last_used, state_json, balance, primary_group, groups_json, active) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE last_used = VALUES(last_used), state_json = VALUES(state_json), " + + "balance = VALUES(balance), primary_group = VALUES(primary_group), groups_json = VALUES(groups_json), active = VALUES(active)"; } else { String filename = cfg.database().endsWith(".db") ? cfg.database() : cfg.database() + ".db"; this.db = Database.sqlite(new File(OumLib.getDataFolder(), filename)); db.runMigrations(ProfileStorage.class, "migrations/sqlite/V1__init.sql", "migrations/sqlite/V2__add_active_column.sql"); + this.saveSql = "INSERT INTO oum_profiles (uuid, name, created_at, last_used, state_json, balance, primary_group, groups_json, active) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT (uuid, name) DO UPDATE SET " + + "last_used = excluded.last_used, state_json = excluded.state_json, balance = excluded.balance, " + + "primary_group = excluded.primary_group, groups_json = excluded.groups_json, active = excluded.active"; } } + private static @NonNull String id(@NonNull UUID uuid) { + return uuid.toString().toLowerCase(Locale.ROOT); + } + public @NonNull Promise> loadAll(@NonNull UUID uuid) { return db.executeQuery( "SELECT name, created_at, last_used, state_json, balance, primary_group, groups_json, active FROM oum_profiles WHERE uuid = ?", @@ -46,42 +61,29 @@ public ProfileStorage(ProfileConfig.@NonNull StorageSection cfg) { rs.getString("groups_json"), rs.getInt("active") == 1 ), - uuid.toString().toLowerCase(Locale.ROOT) + id(uuid) ); } public @NonNull Promise save(@NonNull UUID uuid, @NonNull ProfileData data) { - String id = uuid.toString().toLowerCase(Locale.ROOT); - String sql = mysql - ? "INSERT INTO oum_profiles (uuid, name, created_at, last_used, state_json, balance, primary_group, groups_json, active) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " + - "ON DUPLICATE KEY UPDATE last_used = VALUES(last_used), state_json = VALUES(state_json), " + - "balance = VALUES(balance), primary_group = VALUES(primary_group), groups_json = VALUES(groups_json), active = VALUES(active)" - : "INSERT INTO oum_profiles (uuid, name, created_at, last_used, state_json, balance, primary_group, groups_json, active) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " + - "ON CONFLICT (uuid, name) DO UPDATE SET " + - "last_used = excluded.last_used, state_json = excluded.state_json, balance = excluded.balance, " + - "primary_group = excluded.primary_group, groups_json = excluded.groups_json, active = excluded.active"; return db.executeUpdate( - sql, - id, data.name(), data.createdAt(), data.lastUsed(), data.state().toJson(), + saveSql, + id(uuid), data.name(), data.createdAt(), data.lastUsed(), data.state().toJson(), data.balance(), data.primaryGroup(), data.groupsJson(), data.active() ? 1 : 0 ).map(rows -> null); } public @NonNull Promise setActive(@NonNull UUID uuid, @NonNull String name) { - String id = uuid.toString().toLowerCase(Locale.ROOT); return db.executeUpdate( "UPDATE oum_profiles SET active = CASE WHEN name = ? THEN 1 ELSE 0 END WHERE uuid = ?", - name, id + name, id(uuid) ).map(rows -> null); } public @NonNull Promise rename(@NonNull UUID uuid, @NonNull String oldName, @NonNull String newName) { - String id = uuid.toString().toLowerCase(Locale.ROOT); return db.executeUpdate( "UPDATE oum_profiles SET name = ? WHERE uuid = ? AND name = ?", - newName, id, oldName + newName, id(uuid), oldName ).map(rows -> null); } @@ -89,14 +91,31 @@ public ProfileStorage(ProfileConfig.@NonNull StorageSection cfg) { public @NonNull Promise delete(@NonNull UUID uuid, @NonNull String name) { return db.executeUpdate( "DELETE FROM oum_profiles WHERE uuid = ? AND name = ?", - uuid.toString().toLowerCase(Locale.ROOT), name + id(uuid), name ).map(rows -> null); } + public @NonNull Promise pruneInactive(long cutoffMillis, @NonNull Set excludeUuids) { + if (excludeUuids.isEmpty()) { + return db.executeUpdate("DELETE FROM oum_profiles WHERE last_used < ?", cutoffMillis); + } + StringBuilder sql = new StringBuilder("DELETE FROM oum_profiles WHERE last_used < ? AND uuid NOT IN ("); + Object[] params = new Object[1 + excludeUuids.size()]; + params[0] = cutoffMillis; + int idx = 1; + for (UUID u : excludeUuids) { + if (idx > 1) sql.append(","); + sql.append("?"); + params[idx++] = id(u); + } + sql.append(")"); + return db.executeUpdate(sql.toString(), params); + } + public @NonNull Promise exists(@NonNull UUID uuid, @NonNull String name) { return db.executeQuery( "SELECT 1 FROM oum_profiles WHERE uuid = ? AND name = ?", - uuid.toString().toLowerCase(Locale.ROOT), name + id(uuid), name ).map(rows -> !rows.isEmpty()); } diff --git a/src/main/java/dev/oum/profile/integration/IntegrationManager.java b/src/main/java/dev/oum/profile/integration/IntegrationManager.java index c25496c..e83b870 100644 --- a/src/main/java/dev/oum/profile/integration/IntegrationManager.java +++ b/src/main/java/dev/oum/profile/integration/IntegrationManager.java @@ -7,10 +7,7 @@ import dev.oum.profile.integration.mcmmo.McMMOHandler; import dev.oum.profile.integration.mcmmo.McMMOImpl; import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.HashMap; -import java.util.Map; +import org.jspecify.annotations.NonNull; public final class IntegrationManager { @@ -19,54 +16,27 @@ public final class IntegrationManager { private static final JobsHandler jobs; static { - mcmmo = Bukkit.getPluginManager().isPluginEnabled("mcMMO") ? new McMMOImpl() : new McMMOHandler() { - @Override - public Map capture(Player player) { - return new HashMap<>(); - } - - @Override - public void restore(Player player, Map data) { - } - }; + mcmmo = Bukkit.getPluginManager().isPluginEnabled("mcMMO") ? new McMMOImpl() : McMMOHandler.NOOP; auraSkills = (Bukkit.getPluginManager().isPluginEnabled("AuraSkills") || Bukkit.getPluginManager().isPluginEnabled("AureliumSkills")) - ? new AuraSkillsImpl() : new AuraSkillsHandler() { - @Override - public Map capture(Player player) { - return new HashMap<>(); - } - - @Override - public void restore(Player player, Map data) { - } - }; - - jobs = Bukkit.getPluginManager().isPluginEnabled("Jobs") ? new JobsImpl() : new JobsHandler() { - @Override - public Map capture(Player player) { - return new HashMap<>(); - } + ? new AuraSkillsImpl() : AuraSkillsHandler.NOOP; - @Override - public void restore(Player player, Map data) { - } - }; + jobs = Bukkit.getPluginManager().isPluginEnabled("Jobs") ? new JobsImpl() : JobsHandler.NOOP; } private IntegrationManager() { } - public static McMMOHandler mcmmo() { + public static @NonNull McMMOHandler mcmmo() { return mcmmo; } - public static AuraSkillsHandler auraSkills() { + public static @NonNull AuraSkillsHandler auraSkills() { return auraSkills; } - public static JobsHandler jobs() { + public static @NonNull JobsHandler jobs() { return jobs; } } diff --git a/src/main/java/dev/oum/profile/integration/SkillHandler.java b/src/main/java/dev/oum/profile/integration/SkillHandler.java new file mode 100644 index 0000000..4b319c1 --- /dev/null +++ b/src/main/java/dev/oum/profile/integration/SkillHandler.java @@ -0,0 +1,13 @@ +package dev.oum.profile.integration; + +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; + +import java.util.Map; + +public interface SkillHandler { + + @NonNull Map capture(@NonNull Player player); + + void restore(@NonNull Player player, @NonNull Map data); +} diff --git a/src/main/java/dev/oum/profile/integration/auraskills/AuraSkillsHandler.java b/src/main/java/dev/oum/profile/integration/auraskills/AuraSkillsHandler.java index ba18fc3..ef79560 100644 --- a/src/main/java/dev/oum/profile/integration/auraskills/AuraSkillsHandler.java +++ b/src/main/java/dev/oum/profile/integration/auraskills/AuraSkillsHandler.java @@ -1,12 +1,22 @@ package dev.oum.profile.integration.auraskills; import dev.oum.profile.integration.SkillData; +import dev.oum.profile.integration.SkillHandler; import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; import java.util.Map; -public interface AuraSkillsHandler { - Map capture(Player player); +public interface AuraSkillsHandler extends SkillHandler { - void restore(Player player, Map data); + AuraSkillsHandler NOOP = new AuraSkillsHandler() { + @Override + public @NonNull Map capture(@NonNull Player player) { + return Map.of(); + } + + @Override + public void restore(@NonNull Player player, @NonNull Map data) { + } + }; } diff --git a/src/main/java/dev/oum/profile/integration/jobs/JobsHandler.java b/src/main/java/dev/oum/profile/integration/jobs/JobsHandler.java index 33433cf..35e9b3d 100644 --- a/src/main/java/dev/oum/profile/integration/jobs/JobsHandler.java +++ b/src/main/java/dev/oum/profile/integration/jobs/JobsHandler.java @@ -1,12 +1,22 @@ package dev.oum.profile.integration.jobs; import dev.oum.profile.integration.SkillData; +import dev.oum.profile.integration.SkillHandler; import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; import java.util.Map; -public interface JobsHandler { - Map capture(Player player); +public interface JobsHandler extends SkillHandler { - void restore(Player player, Map data); + JobsHandler NOOP = new JobsHandler() { + @Override + public @NonNull Map capture(@NonNull Player player) { + return Map.of(); + } + + @Override + public void restore(@NonNull Player player, @NonNull Map data) { + } + }; } diff --git a/src/main/java/dev/oum/profile/integration/mcmmo/McMMOHandler.java b/src/main/java/dev/oum/profile/integration/mcmmo/McMMOHandler.java index 95c19c6..075787e 100644 --- a/src/main/java/dev/oum/profile/integration/mcmmo/McMMOHandler.java +++ b/src/main/java/dev/oum/profile/integration/mcmmo/McMMOHandler.java @@ -1,12 +1,22 @@ package dev.oum.profile.integration.mcmmo; import dev.oum.profile.integration.SkillData; +import dev.oum.profile.integration.SkillHandler; import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; import java.util.Map; -public interface McMMOHandler { - Map capture(Player player); +public interface McMMOHandler extends SkillHandler { - void restore(Player player, Map data); + McMMOHandler NOOP = new McMMOHandler() { + @Override + public @NonNull Map capture(@NonNull Player player) { + return Map.of(); + } + + @Override + public void restore(@NonNull Player player, @NonNull Map data) { + } + }; } diff --git a/src/main/java/dev/oum/profile/model/PlayerState.java b/src/main/java/dev/oum/profile/model/PlayerState.java index 4f3dadd..1a4b3f7 100644 --- a/src/main/java/dev/oum/profile/model/PlayerState.java +++ b/src/main/java/dev/oum/profile/model/PlayerState.java @@ -3,10 +3,10 @@ import com.google.gson.Gson; import dev.oum.oumlib.bridge.StatisticsBridge; import dev.oum.oumlib.bridge.economy.EconomyBridge; -import dev.oum.oumlib.util.ItemSerializer; -import dev.oum.oumlib.util.Locations; -import dev.oum.oumlib.util.PotionSerializer; -import dev.oum.profile.config.ProfileConfig; +import dev.oum.oumlib.inventory.ItemSerializer; +import dev.oum.oumlib.inventory.PotionSerializer; +import dev.oum.oumlib.math.Locations; +import dev.oum.profile.config.MainConfig; import dev.oum.profile.integration.IntegrationManager; import dev.oum.profile.integration.SkillData; import org.bukkit.GameMode; @@ -76,7 +76,7 @@ public record PlayerState( } public static @NonNull PlayerState capture(@NonNull Player player, boolean saveLocation, - @NonNull ProfileConfig config, long currentPlaytimeSeconds) { + @NonNull MainConfig config, long currentPlaytimeSeconds) { ItemStack[] invSlots = player.getInventory().getStorageContents(); var maxHpAttr = player.getAttribute(Attribute.MAX_HEALTH); @@ -150,7 +150,7 @@ public record PlayerState( return GSON.fromJson(json, PlayerState.class); } - public void apply(@NonNull Player player, boolean restoreLocation, @NonNull ProfileConfig config) { + public void apply(@NonNull Player player, boolean restoreLocation, @NonNull MainConfig config) { player.getInventory().setStorageContents(ItemSerializer.deserializeArray(inventory)); player.getInventory().setArmorContents(ItemSerializer.deserializeArray(armor)); player.getInventory().setItemInOffHand(ItemSerializer.deserialize(offhand)); diff --git a/src/main/java/dev/oum/profile/profile/ConfirmMenu.java b/src/main/java/dev/oum/profile/profile/ConfirmMenu.java deleted file mode 100644 index b79627c..0000000 --- a/src/main/java/dev/oum/profile/profile/ConfirmMenu.java +++ /dev/null @@ -1,102 +0,0 @@ -package dev.oum.profile.profile; - -import dev.oum.oumlib.bridge.item.ItemBridge; -import dev.oum.oumlib.inventory.ChestMenu; -import dev.oum.oumlib.inventory.ItemBuilder; -import dev.oum.oumlib.scheduler.Scheduler; -import dev.oum.profile.config.ProfileConfig.ConfirmGuiSection; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.jspecify.annotations.NonNull; - -import java.util.ArrayList; -import java.util.List; - -public final class ConfirmMenu { - - private final ConfirmGuiSection config; - private final String targetName; - private final Runnable onConfirm; - private final Runnable onDeny; - - public ConfirmMenu(@NonNull ConfirmGuiSection config, @NonNull String targetName, @NonNull Runnable onConfirm, @NonNull Runnable onDeny) { - this.config = config; - this.targetName = targetName; - this.onConfirm = onConfirm; - this.onDeny = onDeny; - } - - private static @NonNull ItemBuilder resolveItem(@NonNull String input, @NonNull Material fallback) { - if (input.startsWith("head:") || input.startsWith("skull:")) { - String texture = input.substring(input.indexOf(':') + 1); - return ItemBuilder.of(Material.PLAYER_HEAD).skull(texture); - } - return ItemBuilder.of(ItemBridge.getItem(input).orElseGet(() -> new ItemStack(fallback))); - } - - public void open(@NonNull Player player) { - String resolvedTitle = config.title() - .replace("", targetName) - .replace("", targetName); - - char confirmChar = config.confirmSlotChar().isEmpty() ? 'C' : config.confirmSlotChar().charAt(0); - char denyChar = config.denySlotChar().isEmpty() ? 'D' : config.denySlotChar().charAt(0); - - List patternList = config.pattern(); - ChestMenu.Builder builder = ChestMenu.builder() - .title(resolvedTitle) - .rows(config.rows()) - .pattern(patternList.toArray(new String[0])); - - ItemStack borderItem = resolveItem(config.borderMaterial(), Material.GRAY_STAINED_GLASS_PANE) - .name(config.borderName()) - .build(); - - for (String row : patternList) { - for (char ch : row.toCharArray()) { - if (ch != confirmChar && ch != denyChar && ch != ' ') { - builder = builder.bind(ch, borderItem); - } - } - } - - builder = builder.bind(confirmChar, () -> { - List formattedLore = new ArrayList<>(); - for (String line : config.confirmLore()) { - formattedLore.add(line - .replace("", targetName) - .replace("", targetName) - ); - } - - return resolveItem(config.confirmMaterial(), Material.GREEN_WOOL) - .name(config.confirmName().replace("", targetName).replace("", targetName)) - .lore(formattedLore.toArray(new String[0])) - .build(); - }).onClick(confirmChar, ctx -> { - ctx.player().closeInventory(); - Scheduler.runFor(ctx.player(), onConfirm); - }); - - builder = builder.bind(denyChar, () -> { - List formattedLore = new ArrayList<>(); - for (String line : config.denyLore()) { - formattedLore.add(line - .replace("", targetName) - .replace("", targetName) - ); - } - - return resolveItem(config.denyMaterial(), Material.RED_WOOL) - .name(config.denyName().replace("", targetName).replace("", targetName)) - .lore(formattedLore.toArray(new String[0])) - .build(); - }).onClick(denyChar, ctx -> { - ctx.player().closeInventory(); - Scheduler.runFor(ctx.player(), onDeny); - }); - - builder.build().open(player); - } -} diff --git a/src/main/java/dev/oum/profile/profile/ProfileListener.java b/src/main/java/dev/oum/profile/profile/ProfileListener.java index fee5566..c8b7187 100644 --- a/src/main/java/dev/oum/profile/profile/ProfileListener.java +++ b/src/main/java/dev/oum/profile/profile/ProfileListener.java @@ -1,9 +1,8 @@ package dev.oum.profile.profile; import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.config.ConfigManager; +import dev.oum.oumlib.bridge.combat.CombatBridge; import dev.oum.oumlib.event.Events; -import dev.oum.profile.config.ProfileConfig; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; import org.bukkit.event.entity.EntityDamageByEntityEvent; @@ -12,9 +11,11 @@ import org.bukkit.event.player.*; import org.jspecify.annotations.NonNull; +import java.time.Duration; + public final class ProfileListener { - public ProfileListener(@NonNull ProfileManager manager, @NonNull ConfigManager configManager) { + public ProfileListener(@NonNull ProfileManager manager) { Events.listen(PlayerJoinEvent.class) .handler(e -> { OumLib.logDebug("PlayerJoinEvent fired for player " + e.getPlayer().getName()); @@ -28,7 +29,7 @@ public ProfileListener(@NonNull ProfileManager manager, @NonNull ConfigManager

configManager.get().switching().cancelOnMove()) + .filter(e -> manager.config().main().switching().cancelOnMove()) .filter(e -> manager.hasPendingWarmup(e.getPlayer().getUniqueId())) .filter(e -> { var from = e.getFrom(); @@ -37,7 +38,7 @@ public ProfileListener(@NonNull ProfileManager manager, @NonNull ConfigManager

{ OumLib.logDebug("Cancelling profile switch warmup for player " + e.getPlayer().getName() + " due to movement."); - manager.cancelWarmup(e.getPlayer().getUniqueId(), configManager.get().messages().warmupCancelledMove()); + manager.cancelWarmup(e.getPlayer().getUniqueId(), manager.config().messages().warmupCancelledMove()); }); Events.listen(EntityDamageByEntityEvent.class) @@ -52,13 +53,14 @@ public ProfileListener(@NonNull ProfileManager manager, @NonNull ConfigManager

{ Player p = (Player) e.getPlayer(); OumLib.logDebug("Cancelling profile switch warmup for player " + p.getName() + " due to opening an inventory."); - manager.cancelWarmup(p.getUniqueId(), configManager.get().messages().warmupCancelledGeneric()); + manager.cancelWarmup(p.getUniqueId(), manager.config().messages().warmupCancelledGeneric()); }); Events.listen(PlayerDropItemEvent.class) .filter(e -> manager.hasPendingWarmup(e.getPlayer().getUniqueId())) .handler(e -> { OumLib.logDebug("Cancelling profile switch warmup for player " + e.getPlayer().getName() + " due to dropping an item."); - manager.cancelWarmup(e.getPlayer().getUniqueId(), configManager.get().messages().warmupCancelledGeneric()); + manager.cancelWarmup(e.getPlayer().getUniqueId(), manager.config().messages().warmupCancelledGeneric()); }); Events.listen(EntityPickupItemEvent.class) @@ -84,21 +86,21 @@ public ProfileListener(@NonNull ProfileManager manager, @NonNull ConfigManager

{ Player p = (Player) e.getEntity(); OumLib.logDebug("Cancelling profile switch warmup for player " + p.getName() + " due to picking up an item."); - manager.cancelWarmup(p.getUniqueId(), configManager.get().messages().warmupCancelledGeneric()); + manager.cancelWarmup(p.getUniqueId(), manager.config().messages().warmupCancelledGeneric()); }); Events.listen(PlayerInteractEvent.class) .filter(e -> manager.hasPendingWarmup(e.getPlayer().getUniqueId())) .handler(e -> { OumLib.logDebug("Cancelling profile switch warmup for player " + e.getPlayer().getName() + " due to interaction."); - manager.cancelWarmup(e.getPlayer().getUniqueId(), configManager.get().messages().warmupCancelledGeneric()); + manager.cancelWarmup(e.getPlayer().getUniqueId(), manager.config().messages().warmupCancelledGeneric()); }); Events.listen(PlayerTeleportEvent.class) .filter(e -> manager.hasPendingWarmup(e.getPlayer().getUniqueId())) .handler(e -> { OumLib.logDebug("Cancelling profile switch warmup for player " + e.getPlayer().getName() + " due to teleportation."); - manager.cancelWarmup(e.getPlayer().getUniqueId(), configManager.get().messages().warmupCancelledGeneric()); + manager.cancelWarmup(e.getPlayer().getUniqueId(), manager.config().messages().warmupCancelledGeneric()); }); } } \ No newline at end of file diff --git a/src/main/java/dev/oum/profile/profile/ProfileManager.java b/src/main/java/dev/oum/profile/profile/ProfileManager.java index f177a7a..5950f93 100644 --- a/src/main/java/dev/oum/profile/profile/ProfileManager.java +++ b/src/main/java/dev/oum/profile/profile/ProfileManager.java @@ -3,15 +3,16 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.bridge.combat.CombatBridge; import dev.oum.oumlib.bridge.economy.EconomyBridge; import dev.oum.oumlib.bridge.permission.PermissionBridge; -import dev.oum.oumlib.config.ConfigManager; +import dev.oum.oumlib.cooldown.CooldownFormatter; +import dev.oum.oumlib.cooldown.CooldownManager; import dev.oum.oumlib.effect.Sounds; import dev.oum.oumlib.scheduler.Promise; import dev.oum.oumlib.scheduler.Scheduler; import dev.oum.oumlib.scheduler.TaskHandle; import dev.oum.oumlib.text.Text; -import dev.oum.oumlib.util.Cooldown; import dev.oum.profile.ProfilePlaceholders; import dev.oum.profile.api.event.*; import dev.oum.profile.command.Permissions; @@ -19,6 +20,7 @@ import dev.oum.profile.config.ProfileStorage; import dev.oum.profile.model.PlayerState; import dev.oum.profile.model.ProfileData; +import net.kyori.adventure.audience.Audience; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.jspecify.annotations.NonNull; @@ -26,8 +28,9 @@ import java.lang.reflect.Type; import java.time.Duration; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; @@ -37,34 +40,78 @@ public final class ProfileManager { private static final Type STRING_LIST = new TypeToken>() { }.getType(); - private final ConfigManager configManager; + private final ProfileConfig config; private final ProfileStorage storage; private final Map> cache = new ConcurrentHashMap<>(); private final Map active = new ConcurrentHashMap<>(); private final Map warmups = new ConcurrentHashMap<>(); private final Set mutedAlerts = ConcurrentHashMap.newKeySet(); private final Map sessionStarts = new ConcurrentHashMap<>(); - private Cooldown combatCooldown; - private Cooldown switchCooldown; + private final CooldownManager switchCooldown; + private TaskHandle autoSaveTask; - public ProfileManager(@NonNull ConfigManager configManager, @NonNull ProfileStorage storage) { - this.configManager = configManager; + public ProfileManager(@NonNull ProfileConfig config, @NonNull ProfileStorage storage) { + this.config = config; this.storage = storage; - this.combatCooldown = Cooldown.of(Duration.ofSeconds(configManager.get().switching().combatTagDuration())); - this.switchCooldown = Cooldown.of(Duration.ofSeconds(configManager.get().switching().switchCooldownSeconds())); - configManager.onReload(newConfig -> { - this.combatCooldown = Cooldown.of(Duration.ofSeconds(newConfig.switching().combatTagDuration())); - this.switchCooldown = Cooldown.of(Duration.ofSeconds(newConfig.switching().switchCooldownSeconds())); - }); + this.switchCooldown = CooldownManager.create() + .defaultFormatter(CooldownFormatter.COMPACT) + .bypassPredicate(uuid -> { + Player p = Bukkit.getPlayer(uuid); + return p != null && p.hasPermission(Permissions.BYPASS_COOLDOWN); + }); + startAutoSave(); + } + + private static String getActiveName(@NonNull Map map, String defaultName) { + String activeName = null; + for (ProfileData data : map.values()) { + if (data.active()) { + activeName = data.name(); + break; + } + } + if (activeName == null) { + long maxLastUsed = -1; + for (ProfileData data : map.values()) { + if (data.lastUsed() > maxLastUsed) { + maxLastUsed = data.lastUsed(); + activeName = data.name(); + } + } + } + if (activeName == null) { + activeName = defaultName; + } + return activeName; + } + + public void startAutoSave() { + if (autoSaveTask != null) { + autoSaveTask.cancel(); + autoSaveTask = null; + } + var cfg = config.main().autoSave(); + if (cfg != null && cfg.enabled() && cfg.intervalMinutes() > 0) { + long seconds = cfg.intervalMinutes() * 60L; + this.autoSaveTask = Scheduler.runRepeating( + Duration.ofSeconds(seconds), + Duration.ofSeconds(seconds), + () -> { + OumLib.logDebug("Running scheduled auto-save for active online player profiles."); + saveAllOnline(); + } + ); + OumLib.logDebug("Started auto-save task every " + cfg.intervalMinutes() + " minutes."); + } } public @NonNull NameValidation validateProfileName(@NonNull String name) { if (name.isEmpty() || name.contains(" ")) return NameValidation.EMPTY; - int maxLen = configManager.get().profileNameMaxLength(); + int maxLen = config.main().profileNameMaxLength(); if (maxLen > 0 && name.length() > maxLen) return NameValidation.TOO_LONG; - String regex = configManager.get().profileNameRegex(); + String regex = config.main().profileNameRegex(); if (regex != null && !regex.isEmpty()) { try { if (!Pattern.matches(regex, name)) return NameValidation.INVALID_CHARS; @@ -75,6 +122,40 @@ public ProfileManager(@NonNull ConfigManager configManager, @NonN return NameValidation.VALID; } + public boolean checkNameValidation(@NonNull Audience audience, @NonNull String name) { + NameValidation validation = validateProfileName(name); + var msg = config.messages(); + switch (validation) { + case EMPTY -> { + Text.send(audience, msg.invalidProfileName()); + return false; + } + case TOO_LONG -> { + Text.send(audience, msg.profileNameTooLong(), "max", + String.valueOf(config.main().profileNameMaxLength())); + return false; + } + case INVALID_CHARS -> { + Text.send(audience, msg.profileNameInvalidChars()); + return false; + } + case VALID -> { + return true; + } + } + return true; + } + + public @NonNull DateTimeFormatter dateFormatter() { + try { + return DateTimeFormatter.ofPattern(config.main().dateFormat()) + .withZone(ZoneId.systemDefault()); + } catch (IllegalArgumentException e) { + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .withZone(ZoneId.systemDefault()); + } + } + public boolean toggleAlerts(@NonNull UUID uuid) { if (mutedAlerts.contains(uuid)) { mutedAlerts.remove(uuid); @@ -90,7 +171,7 @@ public boolean wantsAlerts(@NonNull UUID uuid) { } public void sendAlert(@NonNull String alertMessage, Object... placeholders) { - if (!configManager.get().adminAlertsEnabled()) return; + if (!config.main().adminAlertsEnabled()) return; for (Player online : Bukkit.getOnlinePlayers()) { if (Permissions.ALERTS.has(online) && wantsAlerts(online.getUniqueId())) { Text.send(online, alertMessage, placeholders); @@ -98,8 +179,8 @@ public void sendAlert(@NonNull String alertMessage, Object... placeholders) { } } - public @NonNull ConfigManager configManager() { - return configManager; + public @NonNull ProfileConfig config() { + return config; } public @NonNull ProfileStorage storage() { @@ -112,8 +193,8 @@ public long getElapsedSessionSeconds(@NonNull UUID uuid) { return (System.currentTimeMillis() - start) / 1000; } - public @NonNull Cooldown combatCooldown() { - return combatCooldown; + public @NonNull CooldownManager switchCooldown() { + return switchCooldown; } public void loadPlayer(@NonNull Player player) { @@ -124,7 +205,7 @@ public void loadPlayer(@NonNull Player player) { for (ProfileData data : list) { map.put(data.name(), data); } - String defaultName = configManager.get().defaultProfileName(); + String defaultName = config.main().defaultProfileName(); if (map.isEmpty()) { OumLib.logDebug("No profiles found for " + player.getName() + ". Creating default profile: " + defaultName); ProfileData def = ProfileData.fresh(defaultName); @@ -133,25 +214,7 @@ public void loadPlayer(@NonNull Player player) { storage.save(uuid, def); } cache.put(uuid, map); - String activeName = null; - for (ProfileData data : map.values()) { - if (data.active()) { - activeName = data.name(); - break; - } - } - if (activeName == null) { - long maxLastUsed = -1; - for (ProfileData data : map.values()) { - if (data.lastUsed() > maxLastUsed) { - maxLastUsed = data.lastUsed(); - activeName = data.name(); - } - } - } - if (activeName == null) { - activeName = defaultName; - } + String activeName = getActiveName(map, defaultName); active.put(uuid, activeName); OumLib.logDebug("Loaded " + map.size() + " profiles for " + player.getName() + ". Active profile: " + activeName); @@ -163,7 +226,7 @@ public void loadPlayer(@NonNull Player player) { ProfileData toApply = map.get(activeName); player.closeInventory(); - toApply.state().apply(player, configManager.get().switching().saveLocation(), configManager.get()); + toApply.state().apply(player, config.main().switching().saveLocation(), config.main()); applyEconomies(player, toApply); sessionStarts.put(uuid, System.currentTimeMillis()); @@ -207,7 +270,7 @@ public void unloadPlayer(@NonNull Player player) { if (data == null) return; player.closeInventory(); - captureLiveState(player, data, configManager.get().switching().saveLocation()); + captureLiveState(player, data, config.main().switching().saveLocation()); storage.save(uuid, data); OumLib.logDebug("Saved active profile '" + activeName + "' data for player " + player.getName()); @@ -217,13 +280,8 @@ public void unloadPlayer(@NonNull Player player) { sessionStarts.remove(uuid); } - public void shutdown() { - OumLib.logDebug("Shutting down ProfileManager. Saving all online players' profile states."); - for (UUID uuid : warmups.keySet()) { - cancelWarmup(uuid); - } - - List> futures = new ArrayList<>(); + public @NonNull Promise saveAllOnline() { + List> promises = new ArrayList<>(); for (Player player : Bukkit.getOnlinePlayers()) { UUID uuid = player.getUniqueId(); Map map = cache.get(uuid); @@ -232,23 +290,46 @@ public void shutdown() { ProfileData data = map.get(activeName); if (data != null) { try { - captureLiveState(player, data, configManager.get().switching().saveLocation()); - futures.add(storage.save(uuid, data).toCompletableFuture()); - OumLib.logDebug("Queued save for active profile '" + activeName + "' for player " + player.getName() + " on shutdown."); + captureLiveState(player, data, config.main().switching().saveLocation()); + promises.add(storage.save(uuid, data)); } catch (Exception e) { - OumLib.logError("Failed to capture state for player " + player.getName() + " on shutdown", e); + OumLib.logError("Failed to capture state for player " + player.getName() + " during save", e); } } } } + if (promises.isEmpty()) return Promise.empty(); + return Promise.allVoid(promises); + } - if (!futures.isEmpty()) { - try { - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - OumLib.logDebug("All " + futures.size() + " profile saves completed on shutdown."); - } catch (Exception e) { - OumLib.logError("Error during batch profile save on shutdown", e); - } + public @NonNull Promise pruneInactiveProfiles(int days) { + if (days <= 0) { + return Promise.completed(0); + } + long cutoffMillis = System.currentTimeMillis() - (days * 86_400_000L); + Set onlineUuids = new HashSet<>(); + for (Player p : Bukkit.getOnlinePlayers()) { + onlineUuids.add(p.getUniqueId()); + } + OumLib.logDebug("Pruning inactive profiles older than " + days + " days (cutoff: " + cutoffMillis + ")"); + return storage.pruneInactive(cutoffMillis, onlineUuids); + } + + public void shutdown() { + if (autoSaveTask != null) { + autoSaveTask.cancel(); + autoSaveTask = null; + } + OumLib.logDebug("Shutting down ProfileManager. Saving all online players' profile states."); + for (UUID uuid : warmups.keySet()) { + cancelWarmup(uuid); + } + + try { + saveAllOnline().join(); + OumLib.logDebug("All profile saves completed on shutdown."); + } catch (Exception e) { + OumLib.logError("Error during batch profile save on shutdown", e); } cache.clear(); @@ -269,7 +350,7 @@ private void captureLiveState(@NonNull Player player, @NonNull ProfileData data, long totalPlaytime = prevPlaytime + elapsed; sessionStarts.put(uuid, System.currentTimeMillis()); - data.setState(PlayerState.capture(player, saveLocation, configManager.get(), totalPlaytime)); + data.setState(PlayerState.capture(player, saveLocation, config.main(), totalPlaytime)); data.setBalance(EconomyBridge.balance(player)); data.setLastUsed(System.currentTimeMillis()); @@ -304,7 +385,7 @@ public boolean hasProfile(@NonNull UUID uuid, @NonNull String name) { public int getMaxProfiles(@NonNull Player player) { if (player.hasPermission(Permissions.MAX_UNLIMITED)) return Integer.MAX_VALUE; int max = 1; - List tiers = configManager.get().limitTiers(); + List tiers = config.main().limitTiers(); if (tiers != null) { for (int tier : tiers) { if (player.hasPermission(Permissions.MAX_TIER_PREFIX + tier)) max = Math.max(max, tier); @@ -344,7 +425,7 @@ public boolean createProfile(@NonNull Player player, @NonNull String name) { map.put(name, data); storage.save(uuid, data); OumLib.logDebug("Profile '" + name + "' created and saved for player " + player.getName()); - sendAlert(configManager.get().messages().adminAlertCreate(), "player", player.getName(), "name", name); + sendAlert(config.messages().adminAlertCreate(), "player", player.getName(), "name", name); return true; } @@ -360,7 +441,7 @@ public boolean deleteProfile(@NonNull Player player, @NonNull String name) { OumLib.logDebug("Profile deletion failed: Cannot delete active profile '" + name + "' for " + player.getName()); return false; } - if (name.equalsIgnoreCase(configManager.get().defaultProfileName())) { + if (name.equalsIgnoreCase(config.main().defaultProfileName())) { OumLib.logDebug("Profile deletion failed: Cannot delete default profile '" + name + "' for " + player.getName()); return false; } @@ -379,7 +460,7 @@ public boolean deleteProfile(@NonNull Player player, @NonNull String name) { map.remove(name); storage.delete(uuid, name); OumLib.logDebug("Profile '" + name + "' deleted successfully for player " + player.getName()); - sendAlert(configManager.get().messages().adminAlertDelete(), "player", player.getName(), "name", name); + sendAlert(config.messages().adminAlertDelete(), "player", player.getName(), "name", name); return true; } @@ -402,7 +483,7 @@ public boolean renameProfile(@NonNull Player player, @NonNull String oldName, @N OumLib.logDebug("Profile rename failed: Profile '" + newName + "' already exists for " + player.getName()); return false; } - if (oldName.equalsIgnoreCase(configManager.get().defaultProfileName())) { + if (oldName.equalsIgnoreCase(config.main().defaultProfileName())) { OumLib.logDebug("Profile rename failed: Cannot rename default profile '" + oldName + "' for " + player.getName()); return false; } @@ -423,7 +504,7 @@ public boolean renameProfile(@NonNull Player player, @NonNull String oldName, @N storage.rename(uuid, oldName, newName); OumLib.logDebug("Profile '" + oldName + "' renamed to '" + newName + "' for player " + player.getName()); - sendAlert(configManager.get().messages().adminAlertRename(), "player", player.getName(), "old", oldName, "new", newName); + sendAlert(config.messages().adminAlertRename(), "player", player.getName(), "old", oldName, "new", newName); return true; } @@ -433,21 +514,21 @@ public void requestSwitch(@NonNull Player player, @NonNull String target) { OumLib.logDebug("Player " + player.getName() + " requested profile switch from '" + currentName + "' to '" + target + "'"); if (!hasProfile(uuid, target)) { - Text.send(player, configManager.get().messages().profileNotFound(), "target", target); + Text.send(player, config.messages().profileNotFound(), "target", target); return; } if (target.equals(currentName)) { - Text.send(player, configManager.get().messages().profileAlreadyActive()); + Text.send(player, config.messages().profileAlreadyActive()); return; } - if (switchCooldown.isOnCooldown(uuid) && !player.hasPermission(Permissions.BYPASS_COOLDOWN)) { - String seconds = String.format("%.1f", switchCooldown.remainingSecondsDouble(uuid)); - Text.send(player, configManager.get().messages().switchCooldown(), "seconds", seconds); + if (switchCooldown.isOnCooldown(uuid)) { + String remaining = switchCooldown.formatRemaining(uuid); + Text.send(player, config.messages().switchCooldown(), "seconds", remaining); return; } - if (configManager.get().switching().cancelInCombat() && combatCooldown.isOnCooldown(uuid) + if (config.main().switching().cancelInCombat() && CombatBridge.isInCombat(player) && !player.hasPermission(Permissions.BYPASS_COMBAT)) { - Text.send(player, configManager.get().messages().combatBlock()); + Text.send(player, config.messages().combatBlock()); return; } @@ -459,17 +540,17 @@ public void requestSwitch(@NonNull Player player, @NonNull String target) { } boolean bypassWarmup = player.hasPermission(Permissions.BYPASS_WARMUP); - if (!configManager.get().switching().warmupEnabled() || bypassWarmup) { + if (!config.main().switching().warmupEnabled() || bypassWarmup) { OumLib.logDebug("Bypassing warmup for player " + player.getName() + " (Warmup config disabled: " - + !configManager.get().switching().warmupEnabled() + ", Permission bypass: " + bypassWarmup + ")"); + + !config.main().switching().warmupEnabled() + ", Permission bypass: " + bypassWarmup + ")"); performSwitch(player, target); return; } cancelWarmup(uuid); - int seconds = configManager.get().switching().warmupSeconds(); + int seconds = config.main().switching().warmupSeconds(); OumLib.logDebug("Starting switch warmup of " + seconds + "s for player " + player.getName()); - Text.send(player, configManager.get().messages().warmupStart(), "target", target, "seconds", String.valueOf(seconds)); + Text.send(player, config.messages().warmupStart(), "target", target, "seconds", String.valueOf(seconds)); final int[] remaining = {seconds}; showWarmupTick(player, remaining[0], target); @@ -516,7 +597,7 @@ public boolean hasPendingWarmup(@NonNull UUID uuid) { } private void showWarmupTick(@NonNull Player player, int remaining, @NonNull String target) { - var cfg = configManager.get().switching(); + var cfg = config.main().switching(); if (cfg.warmupTitleEnabled() && cfg.warmupTitleText() != null) { String title = cfg.warmupTitleText() .replace("", String.valueOf(remaining)) @@ -537,7 +618,7 @@ private void showWarmupTick(@NonNull Player player, int remaining, @NonNull Stri } private void showWarmupComplete(@NonNull Player player) { - var cfg = configManager.get().switching(); + var cfg = config.main().switching(); if (cfg.warmupTitleEnabled()) { player.clearTitle(); } @@ -550,7 +631,7 @@ private void showWarmupComplete(@NonNull Player player) { } private void showWarmupCancel(@NonNull Player player) { - var cfg = configManager.get().switching(); + var cfg = config.main().switching(); if (cfg.warmupTitleEnabled()) { player.clearTitle(); } @@ -582,12 +663,12 @@ public void performSwitch(@NonNull Player player, @NonNull String target) { player.closeInventory(); OumLib.logDebug("Saving current live data for player " + player.getName() + " on profile " + currentName); - captureLiveState(player, current, configManager.get().switching().saveLocation()); + captureLiveState(player, current, config.main().switching().saveLocation()); current.setActive(false); storage.save(uuid, current); OumLib.logDebug("Applying profile '" + target + "' data state to player " + player.getName()); - targetData.state().apply(player, configManager.get().switching().saveLocation(), configManager.get()); + targetData.state().apply(player, config.main().switching().saveLocation(), config.main()); targetData.setLastUsed(System.currentTimeMillis()); targetData.setActive(true); @@ -603,23 +684,23 @@ public void performSwitch(@NonNull Player player, @NonNull String target) { active.put(uuid, target); storage.save(uuid, targetData); storage.setActive(uuid, target); - switchCooldown.set(uuid); + switchCooldown.apply(uuid, Duration.ofSeconds(config.main().switching().switchCooldownSeconds())); - Text.send(player, configManager.get().messages().switchSuccess(), "target", target); + Text.send(player, config.messages().switchSuccess(), "target", target); OumLib.logDebug("Player " + player.getName() + " successfully switched to profile: " + target); - sendAlert(configManager.get().messages().adminAlertSwitch(), "player", player.getName(), "target", target); + sendAlert(config.messages().adminAlertSwitch(), "player", player.getName(), "target", target); Bukkit.getPluginManager().callEvent(new ProfilePostSwitchEvent(player, currentName, target)); }); } private void applyEconomies(@NonNull Player player, @NonNull ProfileData targetData) { - ProfileConfig config = configManager.get(); - if (config.economy() != null && config.economy().enabled()) { + var cfg = config.main(); + if (cfg.economy() != null && cfg.economy().enabled()) { Map savedCurrencies = targetData.state().currencies(); if (savedCurrencies == null) { savedCurrencies = new HashMap<>(); } - for (String currency : config.economy().currencies()) { + for (String currency : cfg.economy().currencies()) { try { double currentBal = EconomyBridge.balance(currency, player); double targetBal = savedCurrencies.getOrDefault(currency, 0.0); diff --git a/src/main/java/dev/oum/profile/profile/ProfileMenu.java b/src/main/java/dev/oum/profile/profile/ProfileMenu.java index 9423943..444079b 100644 --- a/src/main/java/dev/oum/profile/profile/ProfileMenu.java +++ b/src/main/java/dev/oum/profile/profile/ProfileMenu.java @@ -1,18 +1,16 @@ package dev.oum.profile.profile; import dev.oum.oumlib.bridge.economy.EconomyBridge; -import dev.oum.oumlib.bridge.item.ItemBridge; import dev.oum.oumlib.bridge.permission.PermissionBridge; -import dev.oum.oumlib.inventory.ChestMenu; -import dev.oum.oumlib.inventory.ClickAction; -import dev.oum.oumlib.inventory.ItemBuilder; -import dev.oum.oumlib.inventory.Layout; +import dev.oum.oumlib.inventory.*; import dev.oum.oumlib.scheduler.Scheduler; import dev.oum.oumlib.scheduler.TaskHandle; +import dev.oum.oumlib.text.Format; import dev.oum.oumlib.text.Text; import dev.oum.oumlib.text.TextInput; -import dev.oum.oumlib.util.Format; import dev.oum.profile.command.Permissions; +import dev.oum.profile.config.MenusConfig; +import dev.oum.profile.config.MenusConfig.ConfirmGuiSection; import dev.oum.profile.config.ProfileConfig; import dev.oum.profile.integration.IntegrationManager; import dev.oum.profile.integration.SkillData; @@ -27,8 +25,6 @@ import java.time.Duration; import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -44,27 +40,9 @@ public ProfileMenu(@NonNull ProfileManager manager) { this.manager = manager; } - private static @NonNull ItemBuilder resolveItem(@NonNull String input, @NonNull Material fallback) { - if (input.startsWith("head:") || input.startsWith("skull:")) { - String texture = input.substring(input.indexOf(':') + 1); - return ItemBuilder.of(Material.PLAYER_HEAD).skull(texture); - } - return ItemBuilder.of(ItemBridge.getItem(input).orElseGet(() -> new ItemStack(fallback))); - } - - private @NonNull DateTimeFormatter dateFormatter() { - try { - return DateTimeFormatter.ofPattern(manager.configManager().get().dateFormat()) - .withZone(ZoneId.systemDefault()); - } catch (IllegalArgumentException e) { - return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") - .withZone(ZoneId.systemDefault()); - } - } - public void open(@NonNull Player player) { - ProfileConfig mainConfig = manager.configManager().get(); - ProfileConfig.GuiSection cfg = mainConfig.gui(); + ProfileConfig config = manager.config(); + MenusConfig.GuiSection cfg = config.menus().gui(); Map profiles = manager.getProfiles(player.getUniqueId()); String activeProfile = manager.getActiveProfileName(player.getUniqueId()); @@ -91,17 +69,10 @@ public void open(@NonNull Player player) { } // Bind border characters dynamically - ItemStack borderItem = resolveItem(cfg.borderMaterial(), Material.GRAY_STAINED_GLASS_PANE) + ItemStack borderItem = ItemBuilder.from(cfg.borderMaterial(), Material.GRAY_STAINED_GLASS_PANE) .name(cfg.borderName()) .build(); - - for (String row : patternList) { - for (char ch : row.toCharArray()) { - if (ch != pChar && ch != cChar && ch != ' ') { - builder = builder.bind(ch, borderItem); - } - } - } + builder = builder.bindBorders(borderItem, pChar, cChar); // Bind Create Profile button ('C') builder = builder.bind(cChar, () -> { @@ -123,14 +94,14 @@ public void open(@NonNull Player player) { ); } - return resolveItem(matStr, fallback) + return ItemBuilder.from(matStr, fallback) .name(displayName) .lore(formattedLore.toArray(new String[0])) .build(); }).onClick(cChar, ctx -> { int max = manager.getMaxProfiles(player); if (profiles.size() >= max) { - Text.send(player, mainConfig.messages().maxProfilesReached()); + Text.send(player, config.messages().maxProfilesReached()); playErrorSound(player, cfg); return; } @@ -161,7 +132,7 @@ public void open(@NonNull Player player) { final int index = i; builder = builder.item(slot, () -> { if (index >= profileList.size()) { - return resolveItem(cfg.emptySlotMaterial(), Material.LIGHT_GRAY_STAINED_GLASS_PANE) + return ItemBuilder.from(cfg.emptySlotMaterial(), Material.LIGHT_GRAY_STAINED_GLASS_PANE) .name(cfg.emptySlotName()) .build(); } @@ -181,8 +152,8 @@ public void open(@NonNull Player player) { List formattedLore = new ArrayList<>(); for (String line : rawLore) { formattedLore.add(line - .replace("", dateFormatter().format(Instant.ofEpochMilli(data.createdAt()))) - .replace("", dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))) + .replace("", manager.dateFormatter().format(Instant.ofEpochMilli(data.createdAt()))) + .replace("", manager.dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))) .replace("", String.format(Locale.ROOT, "%.2f", data.balance())) .replace("", data.primaryGroup() != null ? data.primaryGroup() : "default") .replace("", data.state().playtimeSeconds() != null @@ -193,7 +164,7 @@ public void open(@NonNull Player player) { ); } - var item = resolveItem(matStr, fallback) + var item = ItemBuilder.from(matStr, fallback) .name(displayName) .lore(formattedLore.toArray(new String[0])); @@ -210,31 +181,31 @@ public void open(@NonNull Player player) { }; if (isRightClick) { if (isActive) { - Text.send(player, mainConfig.messages().cannotDeleteActive()); + Text.send(player, config.messages().cannotDeleteActive()); playErrorSound(player, cfg); return; } - if (data.name().equalsIgnoreCase(manager.configManager().get().defaultProfileName())) { - Text.send(player, mainConfig.messages().cannotDeleteDefault()); + if (data.name().equalsIgnoreCase(config.main().defaultProfileName())) { + Text.send(player, config.messages().cannotDeleteDefault()); playErrorSound(player, cfg); return; } - if (mainConfig.confirmDelete().enabled()) { - new ConfirmMenu(mainConfig.confirmDelete(), data.name(), () -> { + if (config.menus().confirmDelete().enabled()) { + openConfirmDialog(player, config.menus().confirmDelete(), data.name(), () -> { if (manager.deleteProfile(player, data.name())) { - Text.send(player, mainConfig.messages().deleteSuccess(), "name", data.name()); + Text.send(player, config.messages().deleteSuccess(), "name", data.name()); open(player); } else { - Text.send(player, mainConfig.messages().deleteFail(), "name", data.name()); + Text.send(player, config.messages().deleteFail(), "name", data.name()); playErrorSound(player, cfg); } - }, () -> open(player)).open(player); + }, () -> open(player)); } else { if (manager.deleteProfile(player, data.name())) { - Text.send(player, mainConfig.messages().deleteSuccess(), "name", data.name()); + Text.send(player, config.messages().deleteSuccess(), "name", data.name()); open(player); } else { - Text.send(player, mainConfig.messages().deleteFail(), "name", data.name()); + Text.send(player, config.messages().deleteFail(), "name", data.name()); playErrorSound(player, cfg); } } @@ -279,15 +250,15 @@ public void open(@NonNull Player player) { taskRef.set(task); } - private void playErrorSound(@NonNull Player player, ProfileConfig.@NonNull GuiSection cfg) { + private void playErrorSound(@NonNull Player player, MenusConfig.@NonNull GuiSection cfg) { if (cfg.errorSoundEnabled() && cfg.errorSoundKey() != null && !cfg.errorSoundKey().isEmpty()) { player.playSound(Sound.sound(Key.key(cfg.errorSoundKey()), Sound.Source.MASTER, 1.0f, 1.0f)); } } private void openCreationInput(@NonNull Player player) { - ProfileConfig mainConfig = manager.configManager().get(); - ProfileConfig.GuiSection cfg = mainConfig.gui(); + ProfileConfig config = manager.config(); + MenusConfig.GuiSection cfg = config.menus().gui(); TextInput.builder() .prompt(Text.parse(cfg.promptMessage())) @@ -295,62 +266,49 @@ private void openCreationInput(@NonNull Player player) { .cancelWord(cfg.cancelWord()) .onInput((p, text) -> { String clean = text.trim(); - ProfileManager.NameValidation validation = manager.validateProfileName(clean); - switch (validation) { - case EMPTY -> { - Text.send(p, mainConfig.messages().invalidProfileName()); - return false; - } - case TOO_LONG -> { - Text.send(p, mainConfig.messages().profileNameTooLong(), "max", String.valueOf(mainConfig.profileNameMaxLength())); - return false; - } - case INVALID_CHARS -> { - Text.send(p, mainConfig.messages().profileNameInvalidChars()); - return false; - } - default -> { - } + if (!manager.checkNameValidation(p, clean)) { + return false; } if (!p.hasPermission(Permissions.CREATE_PREFIX + clean) && !p.hasPermission(Permissions.CREATE_ALL)) { - Text.send(p, mainConfig.messages().noPermission(), "name", clean); + Text.send(p, config.messages().noPermission(), "name", clean); return false; } - if (mainConfig.confirmCreate().enabled()) { - new ConfirmMenu(mainConfig.confirmCreate(), clean, () -> { + if (config.menus().confirmCreate().enabled()) { + openConfirmDialog(p, config.menus().confirmCreate(), clean, () -> { if (manager.createProfile(p, clean)) { - Text.send(p, mainConfig.messages().createSuccess(), "name", clean); + Text.send(p, config.messages().createSuccess(), "name", clean); open(p); } else { - Text.send(p, mainConfig.messages().createFail(), "name", clean); + Text.send(p, config.messages().createFail(), "name", clean); } }, () -> { - Text.send(p, mainConfig.messages().profileCreationCancelled()); + Text.send(p, config.messages().profileCreationCancelled()); open(p); - }).open(p); + }); return true; } if (manager.createProfile(p, clean)) { - Text.send(p, mainConfig.messages().createSuccess(), "name", clean); + Text.send(p, config.messages().createSuccess(), "name", clean); open(p); return true; } else { - Text.send(p, mainConfig.messages().createFail(), "name", clean); + Text.send(p, config.messages().createFail(), "name", clean); return false; } }) .onCancel(p -> { - Text.send(p, mainConfig.messages().profileCreationCancelled()); + Text.send(p, config.messages().profileCreationCancelled()); open(p); }) .onTimeout(p -> { - Text.send(p, mainConfig.messages().profileCreationTimedOut()); + Text.send(p, config.messages().profileCreationTimedOut()); open(p); }) .start(player); } - private @NonNull ItemStack buildActiveProfileItem(@NonNull Player player, @NonNull ProfileData data, ProfileConfig.@NonNull GuiSection cfg) { + private @NonNull ItemStack buildActiveProfileItem(@NonNull Player player, + @NonNull ProfileData data, MenusConfig.@NonNull GuiSection cfg) { String matStr = cfg.activeProfileMaterial(); Material fallback = Material.BOOK; @@ -372,8 +330,8 @@ private void openCreationInput(@NonNull Player player) { List formattedLore = new ArrayList<>(); for (String line : rawLore) { formattedLore.add(line - .replace("", dateFormatter().format(Instant.ofEpochMilli(data.createdAt()))) - .replace("", dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))) + .replace("", manager.dateFormatter().format(Instant.ofEpochMilli(data.createdAt()))) + .replace("", manager.dateFormatter().format(Instant.ofEpochMilli(data.lastUsed()))) .replace("", String.format(Locale.ROOT, "%.2f", balanceVal)) .replace("", groupVal) .replace("", Format.duration(Duration.ofSeconds(playtimeSecs))) @@ -383,7 +341,7 @@ private void openCreationInput(@NonNull Player player) { ); } - var item = resolveItem(matStr, fallback) + var item = ItemBuilder.from(matStr, fallback) .name(displayName) .lore(formattedLore.toArray(new String[0])) .glow(); @@ -401,4 +359,45 @@ private void openCreationInput(@NonNull Player player) { } return String.join(", ", list); } + + private void openConfirmDialog(@NonNull Player player, @NonNull ConfirmGuiSection section, @NonNull String targetName, + @NonNull Runnable onConfirm, @NonNull Runnable onDeny) { + String resolvedTitle = section.title() + .replace("", targetName) + .replace("", targetName); + + char confirmChar = section.confirmSlotChar().isEmpty() ? 'C' : section.confirmSlotChar().charAt(0); + char denyChar = section.denySlotChar().isEmpty() ? 'D' : section.denySlotChar().charAt(0); + + ItemStack borderItem = ItemBuilder.from(section.borderMaterial(), Material.GRAY_STAINED_GLASS_PANE) + .name(section.borderName()) + .build(); + + ConfirmMenu.builder() + .title(resolvedTitle) + .rows(section.rows()) + .pattern(section.pattern()) + .confirmSlot(confirmChar) + .denySlot(denyChar) + .border(borderItem) + .confirmItem(() -> ItemBuilder.from(section.confirmMaterial(), Material.GREEN_WOOL) + .name(section.confirmName().replace("", targetName).replace("", targetName)) + .lore(formatLore(section.confirmLore(), targetName)) + .build()) + .denyItem(() -> ItemBuilder.from(section.denyMaterial(), Material.RED_WOOL) + .name(section.denyName().replace("", targetName).replace("", targetName)) + .lore(formatLore(section.denyLore(), targetName)) + .build()) + .onConfirm(onConfirm) + .onDeny(onDeny) + .open(player); + } + + private String @NonNull [] formatLore(@NonNull List raw, @NonNull String targetName) { + List formatted = new ArrayList<>(raw.size()); + for (String line : raw) { + formatted.add(line.replace("", targetName).replace("", targetName)); + } + return formatted.toArray(new String[0]); + } } diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index dee1c22..fc89f71 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,5 +1,5 @@ name: OumProfile -version: 1.2-SNAPSHOT +version: 1.2.0 main: dev.oum.profile.OumProfile api-version: '1.21' folia-supported: true