78 Commits

Author SHA1 Message Date
430f340fd4 Updates to 1.21.4
Updates Spigot version
Updates API version
Fixes invulnerability ticks not working as intended
Removes missing material tag.
Fixes max health attribute.
2025-02-21 18:11:06 +01:00
f09dcbe7ff Updates README, and allows the un-setting of kill plane and obstacle blocks 2024-05-16 15:34:53 +02:00
901b052b25 Improves some things
Makes sure to save the arena after editing allowed or loss trigger damage causes
Makes sure to save and restore player saturation
Adds damage animation and damage sound when faking damage for allowed damage causes
Plays a sound when a player loses
2024-05-16 15:00:16 +02:00
2ec15c69c7 Restores player health upon arena exit 2024-05-16 13:32:58 +02:00
d101c7ed02 Implements #37 and improves material tab-completion 2024-05-16 13:14:33 +02:00
bfc0eb7334 Adds POM relocations 2024-05-03 15:19:15 +02:00
eceb7fc6d2 Makes sure levers are reset on an immediate quit 2024-04-30 16:32:15 +02:00
fb68c18fe6 Implements #42 2024-04-30 15:50:18 +02:00
98b5ea5abe Fixes #38 2024-04-28 21:52:18 +02:00
87788e60a3 Implements #35 2024-04-28 16:21:54 +02:00
3a68d30044 Adds the lever functionality to the README 2024-04-22 14:58:27 +02:00
fdd6d94102 Makes levers clicked in parkour arenas turn off
When a lever is clicked while inside a parkour arena, that lever is saved. Once the last player leaves, or the last player restarts, the saved levers are turned off.
2024-04-22 14:52:19 +02:00
a1f76bcfc2 Fixes #36 2024-04-22 12:41:55 +02:00
d124c0948b Improves output of arena group listing 2024-04-21 18:14:44 +02:00
8574dfebbb Fixes trigger for parkour winning 2024-04-21 05:40:05 +02:00
f2328ecfa3 Improves some wording 2024-04-06 21:45:19 +02:00
a632de5b1f Improves some GUI items, and removes some redundancy 2024-04-05 17:28:40 +02:00
aa39243232 Fix #34 2024-04-05 16:29:12 +02:00
0360ada849 Adds two decimals to best time placeholders 2024-04-04 23:54:48 +02:00
81e652b4ff Stores potion effects for players in arenas 2024-04-04 23:29:56 +02:00
d7e950c53e Improves display of time records 2024-04-04 15:50:38 +02:00
c8fbdec64f Fixes some minor issues
Fixes the tab-completion for the randomly inverted game-mode not being properly matched.
Prevents the usage text from displaying for some join errors
2024-04-04 04:28:44 +02:00
09f29e5f67 Merge pull request #30 from SunNetservers/dev
Arena saving bugfix
2024-04-04 03:07:30 +02:00
7213bd0c70 Fixes README formatting 2024-04-04 03:03:57 +02:00
a23d0c769f Bumps version 2024-04-04 03:01:28 +02:00
142cd5339b Fixes a bug in arena saving
Saving a single arena caused all other arena data of that type to be deleted.
2024-04-04 03:00:03 +02:00
b063bd1088 Bumps version for release 2023-12-29 01:56:05 +01:00
310802b42d Adds obstacle blocks for parkour
This change reverts the advanced hit-box detection for kill plane blocks, giving them a full block's hit-box again.
Instead, obstacle blocks have been added, which have an accurate hit-box, and can trigger a hit from any direction.
The horizontal kill plane hit box option has been removed as it's no longer useful.
2023-10-02 23:05:39 +02:00
fc6bd33e87 Adds ability to specify horizontal hit-box for end-rods and similar 2023-09-27 23:13:03 +02:00
ccf43060dc Adds exact hit-boxes for parkour kill planes 2023-09-27 19:14:18 +02:00
a498e9bad0 Fixes some liquid hitbox problems 2023-09-25 20:50:02 +02:00
d8bf77d317 Fixes visibility of some methods 2023-09-17 15:18:51 +02:00
162aff0c1f Fixes a bug in killBlockNames, and reduces writes 2023-09-17 08:03:03 +02:00
a7cfe36c72 Fixes parkour kill plane block names not being updated 2023-08-18 17:54:39 +02:00
407acf0ea2 Adds LIGHT to the block whitelist 2023-07-31 23:51:44 +02:00
b6bf22d1c7 Fixes incorrect alias for parkourRemove 2023-07-31 23:30:56 +02:00
67144fec06 Bumps version to SNAPSHOT again 2023-07-31 23:01:36 +02:00
e52732433a Fixes some incorrect command usage descriptions 2023-07-31 23:01:03 +02:00
776fc5a757 Fixes some markdown 2023-07-17 23:58:25 +02:00
1e7cdf02fc Makes possible player name placeholders more clear 2023-07-17 22:53:28 +02:00
f3d8be8be2 Adds missing info about the command reward player name placeholder 2023-07-17 18:26:27 +02:00
4bf00243e6 Removes SNAPSHOT from version 2023-07-17 13:06:06 +02:00
ea439bfa33 Adds documentation for the minigames reward command 2023-07-17 13:05:29 +02:00
003c9e8367 Finishes the reward tab completer 2023-07-11 16:36:19 +02:00
9dff407713 Mostly finishes rewards 2023-07-10 17:51:59 +02:00
d7b1695cd9 Adds Vault support for Economy and Permission rewards 2023-07-09 19:21:34 +02:00
2bc6c2c825 Fixes reward messages 2023-07-09 18:52:27 +02:00
f24959c8d1 Merge branch 'dev' into rewards
# Conflicts:
#	src/main/java/net/knarcraft/minigames/config/Message.java
2023-07-09 18:22:23 +02:00
8ea930a5f3 Makes all messages configurable 2023-07-09 18:20:31 +02:00
57183b64f5 Makes looking around a dropper arena possible 2023-06-13 18:42:20 +02:00
ba1a7fff68 Adds note about underscores in arena names 2023-06-12 23:37:41 +02:00
57b3b85fbc Adds a new item to allow arena re-tries
Prevents a NumberFormatException if the record number is invalid
Updates Spigot to 1.20
2023-06-12 23:17:48 +02:00
38839c0287 Re-implements the collision removal to fix a severe bug 2023-05-12 14:31:30 +02:00
de5124c8dd Hopefully prevents an exception when restoring a player's fly state 2023-05-12 13:16:32 +02:00
9c91e11780 Gives players an item to open the arena menu 2023-05-11 17:52:49 +02:00
4513fc2de7 Removes some un-used configuration options 2023-05-11 00:47:48 +02:00
7900eb691f Makes hiding players default to true 2023-05-10 16:44:30 +02:00
d6fb9ab0b9 Makes some GUI changes
Makes the current state of the player toggle visible.
Makes it possible to toggle player visibility outside arenas.
2023-05-10 16:33:01 +02:00
7848a0a028 Implements #27 among other things
It was found that the Spigot API's methods for cancelling player collisions won't work without changing the scoreboard. Because of that, the normal option has been disabled. The invisibility option has also been removed, as that's a bad idea if players can still push each-other.
The toggle player option which is implemented in this commit does disable player collision, so that's the only working way right now.

A potential ConcurrentModificationException has been fixed.
The parkourCheckpoint command has been removed, as the functionality is now available through the API.
2023-05-10 15:14:28 +02:00
00ac0582f4 Adds a partially usable arena menu openable through a command 2023-05-09 15:47:54 +02:00
0704e138ec Adds a command for returning to checkpoint by forcing a loss #28 2023-05-08 18:14:37 +02:00
05d647052b Adds unfinished code for storing arena rewards 2023-05-08 02:15:03 +02:00
fc1902e86a Denies playing hardcore in a no-checkpoint arena 2023-04-28 15:32:51 +02:00
e039840e89 Adds a hardcore game-mode ignoring checkpoints 2023-04-28 15:05:36 +02:00
701cdd81eb Merge pull request #26 from SunNetservers/dev
Improves messages, tab-completion
2023-04-26 15:48:03 +00:00
078a8ed007 Adds a message when a player joins an arena 2023-04-26 17:42:06 +02:00
8d1b841619 Adds tab completions for arena properties 2023-04-26 14:41:54 +02:00
4a3329459e Fixes joining not fixing non-location player options 2023-04-26 13:53:29 +02:00
206a85b23a Improves how lingering arena states are handled on shutdown 2023-04-26 13:47:03 +02:00
401490df58 Greatly improves handling of un-exited sessions #25 2023-04-26 13:12:14 +02:00
b2fbaf0e68 Removes some redundancy between various classes 2023-04-25 13:11:11 +02:00
7c04e91024 Merge branch 'master' into dev 2023-04-18 21:57:46 +02:00
e6bb324da1 Improves messages #16 2023-04-18 17:25:10 +02:00
58e25bcb30 Adds tab-complete filtering #12 2023-04-18 14:13:07 +02:00
cf0962ef70 Makes sure all arena sessions are exited regardless 2023-04-17 22:20:42 +02:00
01a3d0b73c Merge branch 'dev' 2023-04-17 16:12:49 +02:00
ce0a0dbaa7 Adds license section to README 2023-04-17 13:45:21 +02:00
ac70b4bb0d Adds minimal tutorial for parkour creation 2023-04-17 13:43:26 +02:00
110 changed files with 6914 additions and 1709 deletions

167
README.md
View File

@ -1,12 +1,23 @@
# MiniGames
This plugin adds several mini-games.
To create a dropper arena, simply use `/droppercreate <name>`, where \<name> is simply the name used to differentiate
and
recognize the arena. Your location will be used as the spawn location for anyone joining the dropper arena. To start
and recognize the arena. Your location will be used as the spawn location for anyone joining the dropper arena. To start
playing, simply use `/dropperjoin <name>`, where \<name> is the same as you specified upon creation.
To modify the arena, use `/dropperedit <name> <property> <value>`.
To create a parkour arena, simply use `/parkourcreate <name>`, where \<name> is simply the name used to differentiate
and recognize the arena. Your location will be used as the spawn location for anyone joining the dropper arena. To start
playing, simply use `/parkourjoin <name>`, where \<name> is the same as you specified upon creation.
To modify the arena, use `/parkouredit <name> <property> <value>`. Use `/parkouredit checkpointAdd here` to add a
checkpoint at your current location.
Special Parkour behavior: If a lever is toggled while in a parkour arena, that lever is saved, and automatically turned
off once the last arena player leaves, or the last arena player restarts. This allows parkour arenas where lever toggles
are required for progression. It is however possible to cheat by having one player go first, toggling required levers,
and then going through much faster.
## Permissions
The only permission normal players will need is `minigames.join` which is set to true by default.
@ -32,9 +43,11 @@ The only permission normal players will need is `minigames.join` which is set to
## Commands
| Command | Alias | Arguments | Description |
|----------------------------------------|----------|-----------------------------|-------------------------------------------------------------------------------------|
|----------------------------------------|----------|------------------------------|-------------------------------------------------------------------------------------|
| /miniGamesReload | /mreload | | Reloads all data from disk. |
| /miniGamesLeave | /mleave | | Leaves the current mini-game. |
| /miniGamesMenu | /mmenu | | Shows a menu of actions if used while in an arena |
| [/miniGamesReward](#minigamesreward) | /mreward | [See this](#minigamesreward) | Adds or removes rewards for an arena |
| /dropperList | /dlist | | Lists available dropper arenas. |
| [/dropperJoin](#dropperjoin) | /djoin | \<arena> \[mode] | Joins the selected arena. |
| /dropperCreate | /dcreate | \<name> | Creates a new dropper arena with the given name. The spawn is set to your location. |
@ -44,7 +57,7 @@ The only permission normal players will need is `minigames.join` which is set to
| /dropperGroupList | /dglist | \[group] | Lists groups, or the stages of a group if a group is specified. |
| [/dropperGroupSwap](#droppergroupswap) | /dgswap | \<arena1> \<arena2> | Swaps the two arenas in the group's ordered list. |
| /parkourList | /plist | | Lists available parkour arenas. |
| /parkourJoin | /pjoin | \<arena> | Joins the selected arena. |
| [/parkourJoin](#parkourjoin) | /pjoin | \<arena> \[mode] | Joins the selected arena. |
| /parkourCreate | /pcreate | \<name> | Creates a new parkour arena with the given name. The spawn is set to your location. |
| /parkourRemove | /premove | \<arena> | Removes the specified parkour arena. |
| [/parkourEdit](#parkouredit) | /pedit | \<arena> \<option> \[value] | Gets or sets a parkour arena option. |
@ -52,6 +65,55 @@ The only permission normal players will need is `minigames.join` which is set to
| /parkourGroupList | /pglist | \[group] | Lists groups, or the stages of a group if a group is specified. |
| [/parkourGroupSwap](#droppergroupswap) | /pgswap | \<arena1> \<arena2> | Swaps the two arenas in the group's ordered list. |
### Command explanation mini-games
#### /miniGamesReward
This command is used to set the rewards for an arena. Rewards can be set for six conditions; a reward for each time the
arena is cleared, a reward for the first time the arena is cleared, a reward for beating your own least deaths record, a
reward for beating your own least time record, a record for beating the global least deaths record and a record for
beating the global least time record. You can give an item, give money, give a permission or execute a console command
with the winning player as an argument.
Note, you can add as many rewards as you want for each reward condition, so you can add a permission and an amount of
currency on the player's first win for example.
`/mreward add dropper|parkour <name> <condition> <type> [value] [value] ...`
`/mreward clear dropper|parkour <name> <condition>`
| Argument | Type | Usage |
|-----------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| action | add / clear | Whether you are adding a reward or clearing rewards. |
| type | dropper / parkour | The type of arena to change rewards for |
| name | _Arena name_ | The name of the arena to change rewards for |
| condition | WIN / FIRST_WIN / PERSONAL_DEATH_RECORD / PERSONAL_TIME_RECORD / GLOBAL_DEATH_RECORD / GLOBAL_TIME_RECORD | The condition to change rewards for. |
| type | COMMAND / ECONOMY / ITEM / PERMISSION | The type of reward to add |
| value | [See reward types](#reward-types) | Input for the reward type. Valid values depend on the type. |
##### Reward types
###### Economy
This reward requires an argument which is a number above zero, which is the amount of currency granted to players.
###### Permission
This reward requires an argument which is the permission string you want to grant the player.
###### Command
The reward requires the command as an argument. Type the full command with spaces and everything, but omit the `/` at
the beginning of the command. Use %player_name% or anything that matches the
RegEx `[<%(\\[{]player[_\\-]?(name)?[>%)\\]}]` (\<player>. \<player-name>, %player_name%, {player}, etc.) as the
placeholder for the rewarded player's name.
###### Item
If used with no arguments, the item in your main hand is used. You can specify a material in the first argument to give
one item of the specified type. You can specify a positive whole number as the second argument to specify the amount of
items to give.
### Command explanation dropper
#### /dropperJoin
@ -80,13 +142,15 @@ This command allows editing the specified property for the specified dropper are
These are all the options that can be changed for an arena.
| Option | Details |
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the arena. Used mainly to select the arena in commands. |
| spawnLocation | The spawn location of any player joining the arena. Use `56.546,64.0,44.45` to specify coordinates, or `here`, `this` or any other string to select your current location. |
| exitLocation | The location players will be sent to when exiting the arena. If not set, the player will be sent to where they joined from. Valid values are the same as for spawnLocation. |
| verticalVelocity | The vertical velocity set for players in the arena (basically their falling speed). It must be greater than 0, but max 75. `12.565` and other decimals are allowed. |
| horizontalVelocity | The horizontal velocity (technically fly speed) set for players in the arena. It must be between 0 and 1, and cannot be 0. Decimals are allowed. |
| winBlockType | The type of block players must hit to win the arena. It can be any material as long as it's a block, and not a type of air. |
| allowedDamageCauses | A list of damage causes that can affect players in the arena. The damage is simulated, rather than real damage. Reaching 0 health triggers a loss. Note: Fall damage cannot be allowed. |
| lossTriggerDamageCauses | A list of damage causes that will trigger a loss for the arena. Useful for arenas where players need to dodge arrows or similar. Note: Fall damage already causes a loss. |
#### /dropperGroupSet
@ -122,6 +186,17 @@ You could use `/droppergroupswap Sea Savanna` to change the order to:
### Command explanation parkour
#### /parkourJoin
This command is used for joining a dropper arena.
`/parkourjoin <arena> [mode]`
| Argument | Usage |
|----------|-----------------------------------------------------------------------------------------------------------|
| arena | The name of the arena to join. |
| mode | Additional challenge modes can be played after an arena has been cleared once. Available modes: hardcore. |
#### /parkourEdit
This command allows editing the specified property for the specified parkour arena.
@ -137,22 +212,25 @@ This command allows editing the specified property for the specified parkour are
These are all the options that can be changed for an arena.
| Option | Details |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the arena. Used mainly to select the arena in commands. |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the arena. Used mainly to select the arena in commands. Note that underscore (_) cannot be used if you want to utilize placeholders, as it's used to split placeholder arguments. |
| spawnLocation | The spawn location of any player joining the arena. Use `56.546,64.0,44.45` to specify coordinates, or `here`, `this` or any other string to select your current location. |
| exitLocation | The location players will be sent to when exiting the arena. If not set, the player will be sent to where they joined from. Valid values are the same as for spawnLocation. |
| winBlockType | The type of block players must hit to win the arena. It can be any material as long as it's a block, and not a type of air. |
| winLocation | The location players must reach to win the arena (see spawnLocation for valid values). If set, this overrides, and is used instead of, the win block type. |
| checkpointAdd | Adds a new checkpoint to the arena's checkpoints (see spawnLocation for valid values). |
| checkpointClear | Clears all current checkpoints. Give any value to execute. If not given a value, current checkpoints are shown. |
| killPlaneBlocks | A comma-separated list of materials which will force a loss on hit. +WOOL and other [material tags](#notes-about-material-tags) are supported as well. |
| killPlaneBlocks | A comma-separated list of materials which will force a loss when stepped on. +WOOL and other [material tags](#notes-about-material-tags) are supported as well. |
| obstacleBlocks | A comma-separated list of materials which will force a loss when touched from any direction. +WOOL and other [material tags](#notes-about-material-tags) are supported as well. |
| allowedDamageCauses | A list of damage causes that can affect players in the arena. The damage is simulated, rather than real damage. Reaching 0 health triggers a loss. |
| lossTriggerDamageCauses | A list of damage causes that will trigger a loss for the arena. Useful for arenas where players need to dodge arrows or similar. |
## Configuration options
### Shared
| Name | Type | Default | Description |
|-----------------------------------|---------------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|---------------------|------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| liquidHitBoxDepth | -1 < decimal < 0 | -0.8 | This decides how far inside a non-solid block the player must go before detection triggers (-1, 0). The closer to -1 it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases. |
| solidHitBoxDistance | 0 < decimal < 1 | 0.2 | This decides the distance the player must be from a block below them before a hit triggers (0, 1). If too low, the likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near. |
@ -168,8 +246,6 @@ These are all the options that can be changed for an arena.
| mustDoGroupedInSequence | true/false | true | Whether grouped dropper arenas must be played in the correct sequence |
| ignoreRecordsUntilGroupBeatenOnce | true/false | false | Whether records won't be registered unless the player has already beaten all arenas in a group. That means players are required to do a second play-through to register a record for a grouped arena. |
| mustDoNormalModeFirst | true/false | true | Whether a player must do the normal/default game-mode before playing any other game-modes |
| makePlayersInvisible | true/false | false | Whether players should be made invisible while playing in a dropper arena |
| disableHitCollision | true/false | true | Whether players should have their entity hit collision disabled while in an arena. This prevents players from pushing each-other if in the same arena. |
| liquidHitBoxDepth | -1 < decimal < 0 | -0.8 | This decides how far inside a non-solid block the player must go before detection triggers (-1, 0). The closer to -1 it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases. |
| solidHitBoxDistance | 0 < decimal < 1 | 0.2 | This decides the distance the player must be from a block below them before a hit triggers (0, 1). If too low, the likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near. |
| blockWhitelist | list | [see this](#blockwhitelist-default) | A whitelist for which blocks won't trigger a loss when hit/passed through. The win block check happens before the loss check, so even blocks on the whitelist can be used as the win-block. "+" denotes a [material tag](#notes-about-material-tags). |
@ -181,8 +257,8 @@ These are all the options that can be changed for an arena.
| enforceCheckpointOrder | true/false | false | Whether to enforce the order in which a player must reach checkpoints. Enabling this ensures that a player cannot trigger a previous checkpoint by accident. It also ensures players cannot skip a checkpoint, even if the arena layout makes it possible. |
| mustDoGroupedInSequence | true/false | true | Whether grouped dropper arenas must be played in the correct sequence |
| ignoreRecordsUntilGroupBeatenOnce | true/false | false | Whether records won't be registered unless the player has already beaten all arenas in a group. That means players are required to do a second play-through to register a record for a grouped arena. |
| makePlayersInvisible | true/false | false | Whether players should be made invisible while playing in a dropper arena |
| killPlaneBlocks | list | [see this](#killplaneblocks-default) | The types of blocks compromising parkour arenas' kill planes. Add any materials you want to use for the "bottom" of your parkour arenas. +WOOL and other [material tags](#notes-about-material-tags) are supported. |
| obstacleBlocks | list | [see this](#obstacleblocks-default) | The types of blocks treated as obstacles in every direction. +WOOL and other [material tags](#notes-about-material-tags) are supported. |
#### blockWhitelist default:
@ -204,25 +280,61 @@ These are all the options that can be changed for an arena.
- LAVA
- MAGMA_BLOCK
## Record placeholders
#### obstacleBlocks default:
- END_ROD
- LIGHTNING_ROD
- CHAIN
## Placeholders
### Record Placeholders
Player records can be displayed on a leaderboard by using PlaceholderAPI. If you want to display a sign-based
leaderboard, you can use the [Placeholder Signs](https://git.knarcraft.net/EpicKnarvik97/PlaceholderSigns) plugin. The
format for the built-in placeholders is as follows:
`%gameMode_record_recordType_gameModeType_identifierType_identifier_recordPlacing_infoType%`
`%gameMode_record_recordType_gameModeType_identifierType_identifier_recordPosition_infoType%`
| Variable | Values | Description |
|----------------|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------|
|----------------|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| gameMode | dropper / parkour | A selection of which game-mode you are getting a record for |
| record | | This must be as-is. It's a selector in case placeholders are added for more than records. |
| record | | This must be as-is. It's a selector for the type of placeholder to get. |
| recordType | deaths / time | Selects the type of record to get (deaths or time). |
| gameModeType | default / inverted / random | Selects the game-mode to get the record for. |
| gameModeType | default / inverted / random / hardcore | Selects the game-mode to get the record for. |
| identifierType | arena / group | The type of thing the following identifier points to (an arena or an arena group). |
| identifier | ? | An identifier (the name or UUID) for an arena or a group (whichever was chosen as identifierType). |
| recordPlacing | 1 / 2 / 3 / ... | The position of the record to get (1 = first place, 2 = second place, etc.). |
| recordPosition | 1 / 2 / 3 / ... | The position of the record to get (1 = first place, 2 = second place, etc.). |
| infoType | player / value / combined | The type of info to get. Player gets the player name, Value gets the value of the achieved record. Combined gets "Player: Record". |
### Player Placeholders
The number of currently playing players can be displayed using PlaceholderAPI. If you want to display a sign-based
leaderboard, you can use the [Placeholder Signs](https://git.knarcraft.net/EpicKnarvik97/PlaceholderSigns) plugin. The
format for the built-in placeholders is as follows:
`%gameMode_players_playing_gameModeType_identifierType_identifier_infoType_additionalSpecifier%`
| Variable | Values | Description |
|-----------------|----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| gameMode | dropper / parkour | A selection of which game-mode you are getting a record for |
| players_playing | | This must be as-is. |
| gameModeType | default / inverted / random / hardcore / all | Selects the game-mode to get the players for. Note that "all" combines players for all game-modes. |
| identifierType | arena / group | The type of thing the following identifier points to (an arena or an arena group). |
| identifier | ? | An identifier (the name or UUID) for an arena or a group (whichever was chosen as identifierType). |
| playerInfoType | player / count | The type of info to get. Player gets the player name, and count gets the total number of players. |
| playerNumber | 1 / 2 / 3 / ... | The player number to get the name of. Players are numbered after their sorted names. This argument is not used when getting the player count. |
### Max Players Placeholder
`%gameMode_players_max_arenaName%`
| Variable | Values | Description |
|-----------------|-------------------|-------------------------------------------------------------|
| gameMode | dropper / parkour | A selection of which game-mode you are getting a record for |
| players_maximum | | This must be as-is. |
| arenaName | ? | An identifier (the name or UUID) for an arena. |
## Notes about material tags
Where a list of material is allowed, this plugin supports using material tags that specify a set of blocks. This makes
@ -243,3 +355,22 @@ Example tags:
- +DRAGON_IMMUNE
- +FENCE_GATES
- +FENCES
## Language customization
Most or all strings are customizable. If you place a strings.yml file in the plugin folder, it will take
priority over built-in languages. If you want to change strings, look at MiniGames/src/main/resources/strings.yml for
the proper keys. All strings have the format: ENUM: "Displayed string". The enum must be identical as it defines which
string you have changed. All strings belonging to a language are beneath the language code and indented with two spaces.
The easiest way to add a new language is to copy an existing language and paste it into your custom strings.yml and
change strings as necessary. If you don't include all strings, the remaining will use the built-in English translation.
Remember to change the language code to whichever you use for your custom language.
The interval messages are unique in that if several values are separated by comma (option1,option2,option3), a random
message will be chosen each time it's displayed.
## License
MiniGames is licensed under the GNU Public License Version 3.0. This includes every source and resource file. See the
HEADER file for a more detailed license description.

90
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId>
<artifactId>MiniGames</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.3-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MiniGames</name>
@ -31,7 +31,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
@ -40,6 +40,40 @@
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>net.knarcraft.knarlib</pattern>
<shadedPattern>net.knarcraft.minigames.lib.knarlib</shadedPattern>
</relocation>
<relocation>
<pattern>net.knarcraft.knargui</pattern>
<shadedPattern>net.knarcraft.minigames.lib.knargui</shadedPattern>
</relocation>
<relocation>
<pattern>org.jetbrains.annotations</pattern>
<shadedPattern>net.knarcraft.minigames.lib.annotations</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>net.knarcraft:knarlib</artifact>
<includes>
<include>net/knarcraft/knarlib/**</include>
</includes>
</filter>
<filter>
<artifact>net.knarcraft:knargui</artifact>
<includes>
<include>net/knarcraft/knargui/**</include>
</includes>
</filter>
<filter>
<artifact>org.jetbrains:annotations</artifact>
<includes>
<include>org/jetbrains/annotations/**</include>
</includes>
</filter>
</filters>
</configuration>
</execution>
</executions>
@ -62,26 +96,32 @@
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>opencollab-snapshot</id>
<url>https://repo.opencollab.dev/main/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
<version>1.21.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
@ -89,5 +129,35 @@
<version>2.10.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.knarcraft</groupId>
<artifactId>knargui</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.knarcraft</groupId>
<artifactId>knarlib</artifactId>
<version>1.2.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.geysermc.geyser</groupId>
<artifactId>api</artifactId>
<version>2.2.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.geysermc.floodgate</groupId>
<artifactId>api</artifactId>
<version>2.2.2-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,23 +1,40 @@
package net.knarcraft.minigames;
import net.knarcraft.knargui.GUIListener;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.formatting.Translator;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.PlayerVisibilityManager;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaData;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.arena.dropper.DropperPlayerEntryState;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaData;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGameMode;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGroup;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.arena.parkour.ParkourArenaPlayerRegistry;
import net.knarcraft.minigames.arena.parkour.ParkourArenaRecordsRegistry;
import net.knarcraft.minigames.arena.parkour.ParkourPlayerEntryState;
import net.knarcraft.minigames.arena.record.IntegerRecord;
import net.knarcraft.minigames.arena.record.LongRecord;
import net.knarcraft.minigames.arena.reward.CommandReward;
import net.knarcraft.minigames.arena.reward.EconomyReward;
import net.knarcraft.minigames.arena.reward.ItemReward;
import net.knarcraft.minigames.arena.reward.PermissionReward;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.command.LeaveArenaCommand;
import net.knarcraft.minigames.command.MenuCommand;
import net.knarcraft.minigames.command.ReloadCommand;
import net.knarcraft.minigames.command.SetArenaRewardCommand;
import net.knarcraft.minigames.command.SetArenaRewardTabCompleter;
import net.knarcraft.minigames.command.dropper.CreateDropperArenaCommand;
import net.knarcraft.minigames.command.dropper.DropperGroupListCommand;
import net.knarcraft.minigames.command.dropper.DropperGroupSetCommand;
@ -41,23 +58,31 @@ import net.knarcraft.minigames.command.parkour.ParkourGroupSwapCommand;
import net.knarcraft.minigames.command.parkour.RemoveParkourArenaCommand;
import net.knarcraft.minigames.command.parkour.RemoveParkourArenaTabCompleter;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.config.SharedConfiguration;
import net.knarcraft.minigames.container.SerializableMaterial;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.listener.CommandListener;
import net.knarcraft.minigames.listener.DamageListener;
import net.knarcraft.minigames.listener.InteractListener;
import net.knarcraft.minigames.listener.MoveListener;
import net.knarcraft.minigames.listener.PlayerLeaveListener;
import net.knarcraft.minigames.placeholder.DropperRecordExpansion;
import net.knarcraft.minigames.placeholder.ParkourRecordExpansion;
import net.knarcraft.minigames.listener.PlayerStateChangeListener;
import net.knarcraft.minigames.manager.EconomyManager;
import net.knarcraft.minigames.manager.PermissionManager;
import net.knarcraft.minigames.placeholder.DropperExpansion;
import net.knarcraft.minigames.placeholder.ParkourExpansion;
import net.md_5.bungee.api.ChatColor;
import net.milkbowl.vault.economy.Economy;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -76,11 +101,14 @@ public final class MiniGames extends JavaPlugin {
private DropperConfiguration dropperConfiguration;
private ParkourConfiguration parkourConfiguration;
private DropperArenaHandler dropperArenaHandler;
private DropperArenaPlayerRegistry dropperArenaPlayerRegistry;
private DropperRecordExpansion dropperRecordExpansion;
private ParkourRecordExpansion parkourRecordExpansion;
private ArenaPlayerRegistry<DropperArena> dropperArenaPlayerRegistry;
private DropperExpansion dropperExpansion;
private ParkourExpansion parkourExpansion;
private ParkourArenaHandler parkourArenaHandler;
private ParkourArenaPlayerRegistry parkourArenaPlayerRegistry;
private ArenaPlayerRegistry<ParkourArena> parkourArenaPlayerRegistry;
private PlayerVisibilityManager playerVisibilityManager;
private Translator translator;
private StringFormatter stringFormatter;
/**
* Gets an instance of this plugin
@ -114,7 +142,7 @@ public final class MiniGames extends JavaPlugin {
*
* @return <p>A dropper arena player registry</p>
*/
public DropperArenaPlayerRegistry getDropperArenaPlayerRegistry() {
public ArenaPlayerRegistry<DropperArena> getDropperArenaPlayerRegistry() {
return this.dropperArenaPlayerRegistry;
}
@ -123,7 +151,7 @@ public final class MiniGames extends JavaPlugin {
*
* @return <p>A parkour arena player registry</p>
*/
public ParkourArenaPlayerRegistry getParkourArenaPlayerRegistry() {
public ArenaPlayerRegistry<ParkourArena> getParkourArenaPlayerRegistry() {
return this.parkourArenaPlayerRegistry;
}
@ -156,6 +184,33 @@ public final class MiniGames extends JavaPlugin {
return this.parkourConfiguration;
}
/**
* Gets the manager keeping track of player visibility
*
* @return <p>The player visibility manager</p>
*/
public PlayerVisibilityManager getPlayerVisibilityManager() {
return this.playerVisibilityManager;
}
/**
* Gets the translator to get messages from
*
* @return <p>The translator</p>
*/
public Translator getTranslator() {
return this.translator;
}
/**
* Gets the string formatter to get formatted messages from
*
* @return <p>The string formatter</p>
*/
public StringFormatter getStringFormatter() {
return this.stringFormatter;
}
/**
* Gets the current session of the given player
*
@ -163,7 +218,7 @@ public final class MiniGames extends JavaPlugin {
* @return <p>The player's current session, or null if not found</p>
*/
public @Nullable ArenaSession getSession(@NotNull UUID playerId) {
DropperArenaSession dropperArenaSession = dropperArenaPlayerRegistry.getArenaSession(playerId);
ArenaSession dropperArenaSession = dropperArenaPlayerRegistry.getArenaSession(playerId);
if (dropperArenaSession != null) {
return dropperArenaSession;
}
@ -191,13 +246,15 @@ public final class MiniGames extends JavaPlugin {
// Reload configuration
this.reloadConfig();
translator.loadLanguages(this.getDataFolder(), "en",
getConfig().getString("language", "en"));
this.sharedConfiguration.load(this.getConfig());
this.dropperConfiguration.load(this.getConfig());
this.parkourConfiguration.load(this.getConfig());
// Clear record caches
this.dropperRecordExpansion.clearCaches();
this.parkourRecordExpansion.clearCaches();
this.dropperExpansion.clearCaches();
this.parkourExpansion.clearCaches();
}
@Override
@ -216,16 +273,76 @@ public final class MiniGames extends JavaPlugin {
ConfigurationSerialization.registerClass(ParkourArenaData.class);
ConfigurationSerialization.registerClass(ParkourArenaGroup.class);
ConfigurationSerialization.registerClass(ParkourArenaGameMode.class);
ConfigurationSerialization.registerClass(DropperPlayerEntryState.class);
ConfigurationSerialization.registerClass(ParkourPlayerEntryState.class);
ConfigurationSerialization.registerClass(CommandReward.class);
ConfigurationSerialization.registerClass(EconomyReward.class);
ConfigurationSerialization.registerClass(ItemReward.class);
ConfigurationSerialization.registerClass(PermissionReward.class);
ConfigurationSerialization.registerClass(RewardCondition.class);
}
@Override
public void onEnable() {
// Plugin startup logic
instance = this;
// Load configuration
loadConfiguration();
// Register all listeners
registerListeners();
// Register all commands
registerCommands();
// Integrate with other plugins
doPluginIntegration();
}
@Override
public void onDisable() {
// Kill all sessions before exiting
for (DropperArena arena : dropperArenaHandler.getArenas().values()) {
dropperArenaPlayerRegistry.removeForArena(arena, true);
}
for (ParkourArena arena : parkourArenaHandler.getArenas().values()) {
parkourArenaPlayerRegistry.removeForArena(arena, true);
}
}
/**
* Sets up integration with third-party plugins
*/
private void doPluginIntegration() {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
this.dropperExpansion = new DropperExpansion(this);
if (!this.dropperExpansion.register()) {
log(Level.WARNING, "Unable to register PlaceholderAPI dropper expansion!");
}
this.parkourExpansion = new ParkourExpansion(this);
if (!this.parkourExpansion.register()) {
log(Level.WARNING, "Unable to register PlaceholderAPI parkour expansion!");
}
}
if (Bukkit.getPluginManager().getPlugin("Vault") != null) {
setupVault();
} else {
log(Level.WARNING, "Vault not found. Economy and Permission rewards are unavailable.");
}
}
/**
* Loads all configuration values used by this plugin
*/
private void loadConfiguration() {
this.saveDefaultConfig();
getConfig().options().copyDefaults(true);
saveConfig();
reloadConfig();
setupStringFormatter();
this.sharedConfiguration = new SharedConfiguration(this.getConfig());
this.dropperConfiguration = new DropperConfiguration(this.getConfig());
this.parkourConfiguration = new ParkourConfiguration(this.getConfig());
@ -235,55 +352,20 @@ public final class MiniGames extends JavaPlugin {
this.parkourArenaPlayerRegistry = new ParkourArenaPlayerRegistry();
this.parkourArenaHandler = new ParkourArenaHandler(this.parkourArenaPlayerRegistry);
this.parkourArenaHandler.load();
this.playerVisibilityManager = new PlayerVisibilityManager();
}
/**
* Registers all listeners used by this plugin
*/
private void registerListeners() {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new DamageListener(), this);
pluginManager.registerEvents(new MoveListener(this.dropperConfiguration, this.parkourConfiguration), this);
pluginManager.registerEvents(new PlayerLeaveListener(), this);
pluginManager.registerEvents(new PlayerStateChangeListener(), this);
pluginManager.registerEvents(new CommandListener(), this);
registerCommand("miniGamesReload", new ReloadCommand(), null);
registerCommand("miniGamesLeave", new LeaveArenaCommand(), null);
registerCommand("dropperCreate", new CreateDropperArenaCommand(), null);
registerCommand("dropperList", new ListDropperArenaCommand(), null);
registerCommand("dropperJoin", new JoinDropperArenaCommand(), new JoinDropperArenaTabCompleter());
registerCommand("dropperEdit", new EditDropperArenaCommand(this.dropperConfiguration), new EditDropperArenaTabCompleter());
registerCommand("dropperRemove", new RemoveDropperArenaCommand(), new RemoveDropperArenaTabCompleter());
registerCommand("dropperGroupSet", new DropperGroupSetCommand(), null);
registerCommand("dropperGroupSwap", new DropperGroupSwapCommand(), null);
registerCommand("dropperGroupList", new DropperGroupListCommand(), null);
registerCommand("parkourCreate", new CreateParkourArenaCommand(), null);
registerCommand("parkourList", new ListParkourArenaCommand(), null);
registerCommand("parkourJoin", new JoinParkourArenaCommand(), new JoinParkourArenaTabCompleter());
registerCommand("parkourEdit", new EditParkourArenaCommand(), new EditParkourArenaTabCompleter());
registerCommand("parkourRemove", new RemoveParkourArenaCommand(), new RemoveParkourArenaTabCompleter());
registerCommand("parkourGroupSet", new ParkourGroupSetCommand(), null);
registerCommand("parkourGroupSwap", new ParkourGroupSwapCommand(), null);
registerCommand("parkourGroupList", new ParkourGroupListCommand(), null);
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
this.dropperRecordExpansion = new DropperRecordExpansion(this);
if (!this.dropperRecordExpansion.register()) {
log(Level.WARNING, "Unable to register PlaceholderAPI dropper expansion!");
}
this.parkourRecordExpansion = new ParkourRecordExpansion(this);
if (!this.parkourRecordExpansion.register()) {
log(Level.WARNING, "Unable to register PlaceholderAPI parkour expansion!");
}
}
}
@Override
public void onDisable() {
// Throw out currently playing players before exiting
for (Player player : getServer().getOnlinePlayers()) {
ArenaSession session = getSession(player.getUniqueId());
if (session != null) {
session.triggerQuit(true);
}
}
pluginManager.registerEvents(new GUIListener(true), this);
pluginManager.registerEvents(new InteractListener(), this);
}
/**
@ -306,4 +388,82 @@ public final class MiniGames extends JavaPlugin {
}
}
/**
* Registers all commands used by this plugin
*/
private void registerCommands() {
registerCommand("miniGamesReload", new ReloadCommand(), null);
registerCommand("miniGamesLeave", new LeaveArenaCommand(), null);
registerCommand("miniGamesMenu", new MenuCommand(), null);
registerCommand("miniGamesReward", new SetArenaRewardCommand(), new SetArenaRewardTabCompleter());
registerDropperCommands();
registerParkourCommands();
}
/**
* Registers all commands related to droppers
*/
private void registerDropperCommands() {
registerCommand("dropperCreate", new CreateDropperArenaCommand(), null);
registerCommand("dropperList", new ListDropperArenaCommand(), null);
registerCommand("dropperJoin", new JoinDropperArenaCommand(), new JoinDropperArenaTabCompleter());
registerCommand("dropperEdit", new EditDropperArenaCommand(this.dropperConfiguration), new EditDropperArenaTabCompleter());
registerCommand("dropperRemove", new RemoveDropperArenaCommand(), new RemoveDropperArenaTabCompleter());
registerCommand("dropperGroupSet", new DropperGroupSetCommand(), null);
registerCommand("dropperGroupSwap", new DropperGroupSwapCommand(), null);
registerCommand("dropperGroupList", new DropperGroupListCommand(), null);
}
/**
* Registers all commands related to parkour
*/
private void registerParkourCommands() {
registerCommand("parkourCreate", new CreateParkourArenaCommand(), null);
registerCommand("parkourList", new ListParkourArenaCommand(), null);
registerCommand("parkourJoin", new JoinParkourArenaCommand(), new JoinParkourArenaTabCompleter());
registerCommand("parkourEdit", new EditParkourArenaCommand(), new EditParkourArenaTabCompleter());
registerCommand("parkourRemove", new RemoveParkourArenaCommand(), new RemoveParkourArenaTabCompleter());
registerCommand("parkourGroupSet", new ParkourGroupSetCommand(), null);
registerCommand("parkourGroupSwap", new ParkourGroupSwapCommand(), null);
registerCommand("parkourGroupList", new ParkourGroupListCommand(), null);
}
/**
* Sets up the translator and the string formatter
*/
private void setupStringFormatter() {
translator = new Translator();
translator.registerMessageCategory(MiniGameMessage.ERROR_PLAYER_ONLY);
translator.loadLanguages(this.getDataFolder(), "en",
getConfig().getString("language", "en"));
stringFormatter = new StringFormatter(this.getDescription().getName(), translator);
stringFormatter.setColorConversion(ColorConversion.RGB);
stringFormatter.setNamePrefix("#546EED[&r&l");
stringFormatter.setNameSuffix("#546EED]");
stringFormatter.setErrorColor(ChatColor.RED);
stringFormatter.setSuccessColor(ChatColor.GREEN);
}
/**
* Sets up Vault by getting plugins from their providers
*/
private void setupVault() {
ServicesManager servicesManager = this.getServer().getServicesManager();
RegisteredServiceProvider<Permission> permissionProvider = servicesManager.getRegistration(Permission.class);
RegisteredServiceProvider<Economy> economyProvider = servicesManager.getRegistration(Economy.class);
if (permissionProvider != null) {
PermissionManager.initialize(permissionProvider.getProvider());
} else {
log(Level.WARNING, "No Vault permission provider found. Permission rewards are unavailable.");
}
if (economyProvider != null) {
EconomyManager.initialize(economyProvider.getProvider());
} else {
log(Level.WARNING, "No Vault economy provider found. Economy rewards are unavailable.");
}
}
}

View File

@ -0,0 +1,134 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.util.ArenaStorageHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* A player registry to keep track of currently playing players
*
* @param <K> <p>The type of arena stored</p>
*/
public abstract class AbstractArenaPlayerRegistry<K extends Arena> implements ArenaPlayerRegistry<K> {
private final Map<Arena, Set<UUID>> arenaPlayers = new HashMap<>();
private final Map<UUID, ArenaSession> arenaSessions = new HashMap<>();
private final Map<UUID, PlayerEntryState> entryStates = new HashMap<>();
/**
* Instantiates a new arena player registry
*/
public AbstractArenaPlayerRegistry() {
loadEntryStates();
}
@Override
@NotNull
public Set<UUID> getPlayingPlayers() {
return arenaSessions.keySet();
}
@Override
@Nullable
public PlayerEntryState getEntryState(@NotNull UUID playerId) {
return this.entryStates.get(playerId);
}
@Override
public void registerPlayer(@NotNull UUID playerId, @NotNull ArenaSession arenaSession) {
this.arenaSessions.put(playerId, arenaSession);
this.entryStates.put(playerId, arenaSession.getEntryState());
this.arenaPlayers.putIfAbsent(arenaSession.getArena(), new HashSet<>());
this.arenaPlayers.get(arenaSession.getArena()).add(playerId);
this.saveEntryStates();
}
@Override
public boolean removePlayer(@NotNull UUID playerId, boolean restoreState) {
// Try and restore the state. If it cannot be restored, retain the entry state
PlayerEntryState entryState = this.entryStates.remove(playerId);
if (restoreState) {
if (entryState.restore()) {
this.saveEntryStates();
} else {
this.entryStates.put(playerId, entryState);
}
} else {
this.saveEntryStates();
}
if (this.arenaSessions.containsKey(playerId)) {
this.arenaPlayers.get(this.arenaSessions.get(playerId).getArena()).remove(playerId);
}
return this.arenaSessions.remove(playerId) != null;
}
@Override
public @Nullable ArenaSession getArenaSession(@NotNull UUID playerId) {
return this.arenaSessions.getOrDefault(playerId, null);
}
@Override
public void removeForArena(@NotNull K arena, boolean immediately) {
Set<UUID> removed = new HashSet<>();
for (Map.Entry<UUID, ArenaSession> entry : this.arenaSessions.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit(immediately, false);
removed.add(entry.getKey());
}
}
removed.forEach(this.arenaSessions::remove);
}
@Override
@NotNull
public Set<UUID> getPlayingPlayers(@NotNull K arena) {
if (arenaPlayers.containsKey(arena)) {
return arenaPlayers.get(arena);
} else {
return new HashSet<>();
}
}
/**
* Gets a string key unique to this type of player registry
*
* @return <p>A unique key used for entry state storage</p>
*/
protected abstract String getEntryStateStorageKey();
/**
* Saves all entry states to disk
*/
private void saveEntryStates() {
ArenaStorageHelper.storeArenaPlayerEntryStates(getEntryStateStorageKey(), new HashSet<>(entryStates.values()));
}
/**
* Loads all entry states from disk
*/
private void loadEntryStates() {
this.entryStates.clear();
Set<PlayerEntryState> entryStates = ArenaStorageHelper.getArenaPlayerEntryStates(getEntryStateStorageKey());
for (PlayerEntryState entryState : entryStates) {
this.entryStates.put(entryState.getPlayerId(), entryState);
}
if (!this.entryStates.isEmpty()) {
MiniGames.log(Level.WARNING, entryStates.size() + " un-exited sessions found. This happens if " +
"players leave in the middle of a game, or if the server crashes. MiniGames will do its best " +
"to fix the players' states.");
}
}
}

View File

@ -0,0 +1,152 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.property.RecordResult;
import net.knarcraft.minigames.property.RecordType;
import net.knarcraft.minigames.util.PlayerTeleporter;
import net.knarcraft.minigames.util.RewardHelper;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public abstract class AbstractArenaSession implements ArenaSession {
private final @NotNull Arena arena;
private final @NotNull ArenaGameMode gameMode;
private final @NotNull Player player;
protected int deaths;
protected long startTime;
protected PlayerEntryState entryState;
/**
* Instantiates a new abstract arena session
*
* @param arena <p>The arena that's being played in</p>
* @param player <p>The player playing the arena</p>
* @param gameMode <p>The game-mode</p>
*/
public AbstractArenaSession(@NotNull Arena arena, @NotNull Player player, @NotNull ArenaGameMode gameMode) {
this.arena = arena;
this.player = player;
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
}
@Override
public void triggerQuit(boolean immediately, boolean removeSession) {
// Stop this session
if (removeSession) {
removeSession();
}
// Teleport the player out of the arena
teleportToExit(immediately);
// Make the player visible to everyone
MiniGames.getInstance().getPlayerVisibilityManager().showPlayersFor(player);
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(player, MiniGameMessage.SUCCESS_ARENA_QUIT);
}
@Override
public void reset() {
this.deaths = 0;
this.startTime = System.currentTimeMillis();
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), false, false);
this.entryState.setArenaState();
}
/**
* Announces a record set by this player
*
* @param recordResult <p>The result of the record</p>
* @param recordType <p>The type of record set (time or deaths)</p>
*/
protected void announceRecord(@NotNull RecordResult recordResult, @NotNull RecordType recordType) {
if (recordResult == RecordResult.NONE) {
return;
}
// Gets a string representation of the played game-mode
String gameModeString = getGameModeString();
MiniGameMessage recordInfoMiniGameMessage = switch (recordResult) {
case WORLD_RECORD -> MiniGameMessage.RECORD_ACHIEVED_GLOBAL;
case PERSONAL_BEST -> MiniGameMessage.RECORD_ACHIEVED_PERSONAL;
default -> throw new IllegalStateException("Unexpected value: " + recordResult);
};
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
String recordInfo = stringFormatter.replacePlaceholder(recordInfoMiniGameMessage, "{recordType}",
recordType.name().toLowerCase().replace("_", " "));
stringFormatter.displaySuccessMessage(player, stringFormatter.replacePlaceholders(
MiniGameMessage.SUCCESS_RECORD_ACHIEVED, new String[]{"{gameMode}", "{recordInfo}"},
new String[]{gameModeString, recordInfo}));
// Reward the player
rewardRecord(recordResult, recordType);
}
/**
* Registers the player's record if necessary, and prints record information to the player
*/
protected void registerRecord() {
ArenaRecordsRegistry recordsRegistry = this.arena.getData().getRecordRegistries().get(this.gameMode);
long timeElapsed = System.currentTimeMillis() - this.startTime;
announceRecord(recordsRegistry.registerTimeRecord(this.player.getUniqueId(), timeElapsed), RecordType.TIME);
announceRecord(recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths), RecordType.DEATHS);
}
/**
* Rewards the specified achieved record
*
* @param recordResult <p>The result of the record achieved</p>
* @param recordType <p>The type of record achieved</p>
*/
protected void rewardRecord(RecordResult recordResult, RecordType recordType) {
RewardCondition condition = null;
if (recordResult == RecordResult.WORLD_RECORD) {
if (recordType == RecordType.DEATHS) {
condition = RewardCondition.GLOBAL_DEATH_RECORD;
} else if (recordType == RecordType.TIME) {
condition = RewardCondition.GLOBAL_TIME_RECORD;
}
} else if (recordResult == RecordResult.PERSONAL_BEST) {
if (recordType == RecordType.DEATHS) {
condition = RewardCondition.PERSONAL_DEATH_RECORD;
} else if (recordType == RecordType.TIME) {
condition = RewardCondition.PERSONAL_TIME_RECORD;
}
}
RewardHelper.grantRewards(this.player, this.arena.getRewards(condition));
}
/**
* Teleports the playing player out of the arena
*/
protected void teleportToExit(boolean immediately) {
// Teleport the player out of the arena
Location exitLocation;
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
* Gets the string representation of the session's game-mode
*
* @return <p>The string representation</p>
*/
protected abstract String getGameModeString();
/**
* Removes this session from current sessions
*/
protected abstract void removeSession();
}

View File

@ -1,19 +1,34 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.property.PersistentDataKey;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* An abstract representation of a player's entry state
*/
public abstract class AbstractPlayerEntryState implements PlayerEntryState {
protected final Player player;
private final boolean makePlayerInvisible;
protected final UUID playerId;
private final Location entryLocation;
private final boolean originalIsFlying;
private final GameMode originalGameMode;
@ -21,16 +36,17 @@ public abstract class AbstractPlayerEntryState implements PlayerEntryState {
private final boolean originalInvulnerable;
private final boolean originalIsSwimming;
private final boolean originalCollideAble;
private final double originalHealth;
private final float originalSaturation;
private final Collection<PotionEffect> originalPotionEffects;
/**
* Instantiates a new abstract player entry state
*
* @param player <p>The player whose state this should keep track of</p>
* @param makePlayerInvisible <p>Whether players should be made invisible while in the arena</p>
*/
public AbstractPlayerEntryState(@NotNull Player player, boolean makePlayerInvisible) {
this.player = player;
this.makePlayerInvisible = makePlayerInvisible;
public AbstractPlayerEntryState(@NotNull Player player) {
this.playerId = player.getUniqueId();
this.entryLocation = player.getLocation().clone();
this.originalIsFlying = player.isFlying();
this.originalGameMode = player.getGameMode();
@ -38,27 +54,79 @@ public abstract class AbstractPlayerEntryState implements PlayerEntryState {
this.originalInvulnerable = player.isInvulnerable();
this.originalIsSwimming = player.isSwimming();
this.originalCollideAble = player.isCollidable();
}
this.originalHealth = player.getHealth();
this.originalSaturation = player.getSaturation();
@Override
public void setArenaState() {
if (this.makePlayerInvisible) {
this.player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,
PotionEffect.INFINITE_DURATION, 3));
// Store and clear potion effects
this.originalPotionEffects = getPlayer().getActivePotionEffects();
for (PotionEffect potionEffect : this.originalPotionEffects) {
player.removePotionEffect(potionEffect.getType());
}
}
@Override
public void restore() {
this.player.setFlying(this.originalIsFlying);
this.player.setGameMode(this.originalGameMode);
this.player.setAllowFlight(this.originalAllowFlight);
this.player.setInvulnerable(this.originalInvulnerable);
this.player.setSwimming(this.originalIsSwimming);
this.player.setCollidable(this.originalCollideAble);
if (this.makePlayerInvisible) {
this.player.removePotionEffect(PotionEffectType.INVISIBILITY);
/**
* Instantiates a new abstract player entry state
*
* @param playerId <p>The id of the player whose state this should keep track of</p>
* @param entryLocation <p>The location the player entered from</p>
* @param originalIsFlying <p>Whether the player was flying before entering the arena</p>
* @param originalGameMode <p>The game-mode of the player before entering the arena</p>
* @param originalAllowFlight <p>Whether the player was allowed flight before entering the arena</p>
* @param originalInvulnerable <p>Whether the player was invulnerable before entering the arena</p>
* @param originalIsSwimming <p>Whether the player was swimming before entering the arena</p>
* @param originalCollideAble <p>Whether the player was collide-able before entering the arena</p>
* @param originalPotionEffects <p>The potion effects applied to the player when joining</p>
* @param originalHealth <p>The health of the player when joining the arena</p>
* @param originalSaturation <p>The saturation of the player when joining the arena</p>
*/
public AbstractPlayerEntryState(@NotNull UUID playerId, @NotNull Location entryLocation,
boolean originalIsFlying, GameMode originalGameMode, boolean originalAllowFlight,
boolean originalInvulnerable, boolean originalIsSwimming,
boolean originalCollideAble, @NotNull Collection<PotionEffect> originalPotionEffects,
double originalHealth, float originalSaturation) {
this.playerId = playerId;
this.entryLocation = entryLocation;
this.originalIsFlying = originalIsFlying;
this.originalGameMode = originalGameMode;
this.originalAllowFlight = originalAllowFlight;
this.originalInvulnerable = originalInvulnerable;
this.originalIsSwimming = originalIsSwimming;
this.originalCollideAble = originalCollideAble;
this.originalPotionEffects = originalPotionEffects;
this.originalHealth = originalHealth;
this.originalSaturation = originalSaturation;
}
@Override
public @NotNull UUID getPlayerId() {
return this.playerId;
}
@Override
public boolean restore() {
Player player = getPlayer();
if (player == null) {
return false;
}
restore(player);
return true;
}
@Override
public void restore(@NotNull Player player) {
player.setCollidable(this.originalCollideAble);
player.setAllowFlight(this.originalAllowFlight);
player.setFlying(player.getAllowFlight() && this.originalIsFlying);
player.setGameMode(this.originalGameMode);
player.setInvulnerable(this.originalInvulnerable);
player.setSwimming(this.originalIsSwimming);
for (PotionEffect potionEffect : originalPotionEffects) {
player.addPotionEffect(potionEffect);
}
removeMenuItem(player);
player.setHealth(originalHealth);
player.setSaturation(originalSaturation);
player.setFallDistance(0);
}
@Override
@ -66,4 +134,74 @@ public abstract class AbstractPlayerEntryState implements PlayerEntryState {
return this.entryLocation;
}
/**
* Gets the player this entry state belongs to
*
* @return <p>The player, or null if not currently online</p>
*/
protected Player getPlayer() {
Player player = Bukkit.getOfflinePlayer(this.playerId).getPlayer();
if (player == null) {
MiniGames.log(Level.WARNING, "Unable to change state for player with id " + this.playerId +
" because the player was not found on the server.");
}
return player;
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("playerId", new SerializableUUID(this.playerId));
data.put("entryLocation", this.entryLocation);
data.put("originalIsFlying", this.originalIsFlying);
data.put("originalGameMode", this.originalGameMode.name());
data.put("originalAllowFlight", this.originalAllowFlight);
data.put("originalInvulnerable", this.originalInvulnerable);
data.put("originalIsSwimming", this.originalIsSwimming);
data.put("originalCollideAble", this.originalCollideAble);
data.put("originalPotionEffects", this.originalPotionEffects);
data.put("originalHealth", this.originalHealth);
data.put("originalSaturation", this.originalSaturation);
return data;
}
/**
* Removes the menu item from the given player's inventory
*
* @param player <p>The player to remove the menu item from</p>
*/
private void removeMenuItem(Player player) {
Set<ItemStack> itemsToRemove = new HashSet<>();
player.getInventory().forEach((item) -> {
if (item == null) {
return;
}
ItemMeta meta = item.getItemMeta();
if (meta == null) {
return;
}
Integer persistentData = meta.getPersistentDataContainer().get(new NamespacedKey(MiniGames.getInstance(),
PersistentDataKey.MENU_ITEM.getKeyName()), PersistentDataType.INTEGER);
if (persistentData != null && persistentData == PersistentDataKey.MENU_ITEM.getDataValue()) {
itemsToRemove.add(item);
}
});
for (ItemStack toRemove : itemsToRemove) {
player.getInventory().remove(toRemove);
}
}
/**
* Gets a boolean value from a serialization map
*
* @param data <p>The serialization data to look through</p>
* @param key <p>The key to get</p>
* @return <p>The boolean value of the key</p>
*/
protected static boolean getBoolean(Map<String, Object> data, String key) {
Boolean value = (Boolean) data.get(key);
return Objects.requireNonNullElse(value, false);
}
}

View File

@ -1,14 +1,21 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.arena.reward.Reward;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.UUID;
/**
* An interface describing an arena
*/
@SuppressWarnings("unused")
public interface Arena {
/**
@ -16,28 +23,40 @@ public interface Arena {
*
* @return <p>The name of this arena</p>
*/
@NotNull String getArenaName();
@NotNull
String getArenaName();
/**
* Sets the name of this arena
*
* @param arenaName <p>The new name</p>
* @return <p>True if successfully updated</p>
*/
boolean setName(@NotNull String arenaName);
/**
* Gets the data stored for this arena
*
* @return <p>The stored data</p>
*/
@NotNull ArenaData getData();
@NotNull
ArenaData getData();
/**
* Gets the id of this arena
*
* @return <p>This arena's identifier</p>
*/
@NotNull UUID getArenaId();
@NotNull
UUID getArenaId();
/**
* Gets this arena's sanitized name
*
* @return <p>This arena's sanitized name</p>
*/
@NotNull String getArenaNameSanitized();
@NotNull
String getArenaNameSanitized();
/**
* Removes the data file belonging to this arena
@ -53,13 +72,31 @@ public interface Arena {
*/
boolean saveData();
/**
* Gets the type of block a player has to hit to win this arena
*
* @return <p>The kind of block players must hit</p>
*/
@NotNull
Material getWinBlockType();
/**
* Sets the material of the win block type
*
* <p>The win block type is the type of block a player must hit to win in this arena</p>
*
* @param material <p>The material to set for the win block type</p>
* @return <p>True if successfully updated</p>
*/
boolean setWinBlockType(@NotNull Material material);
/**
* Gets whether standing on the given block should cause a win
*
* @param block <p>The block to check</p>
* @return <p>True if standing on the block will cause a win</p>
*/
boolean willCauseWin(Block block);
boolean willCauseWin(@NotNull Block block);
/**
* Gets whether standing on the given block should cause a loss
@ -67,7 +104,7 @@ public interface Arena {
* @param block <p>The block to check</p>
* @return <p>True if standing on the block will cause a loss</p>
*/
boolean willCauseLoss(Block block);
boolean willCauseLoss(@NotNull Block block);
/**
* Gets whether the win location is a solid block
@ -81,6 +118,105 @@ public interface Arena {
*
* @return <p>This arena's spawn location</p>
*/
@NotNull Location getSpawnLocation();
@NotNull
Location getSpawnLocation();
/**
* Sets the spawn location for this arena
*
* @param newLocation <p>The new spawn location</p>
* @return <p>True if successfully updated</p>
*/
boolean setSpawnLocation(@Nullable Location newLocation);
/**
* Gets this arena's exit location
*
* @return <p>This arena's exit location, or null if no such location is set.</p>
*/
@Nullable
Location getExitLocation();
/**
* Sets the exit location for this arena
*
* @param newLocation <p>The new exit location</p>
* @return <p>True if successfully updated</p>
*/
boolean setExitLocation(@Nullable Location newLocation);
/**
* Adds a reward to this arena
*
* @param rewardCondition <p>The condition for the reward to be granted</p>
* @param reward <p>The reward to be granted</p>
*/
void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward);
/**
* Clears this arena's rewards for the given condition
*
* @param rewardCondition <p>The reward condition to clear all rewards for</p>
*/
void clearRewards(@NotNull RewardCondition rewardCondition);
/**
* Gets all rewards for the given reward condition
*
* @param rewardCondition <p>The condition to get the rewards for</p>
* @return <p>All rewards</p>
*/
@NotNull
Set<Reward> getRewards(RewardCondition rewardCondition);
/**
* Gets the maximum amount of players that can join this arena at once
*
* @return <p>The maximum amount of players</p>
*/
int getMaxPlayers();
/**
* Sets the maximum amount of players that can join this arena at once
*
* @param newValue <p>The new maximum amount of players</p>
*/
boolean setMaxPlayers(int newValue);
/**
* Gets the damage causes that won't be blocked for this arena
*
* <p>Receiving any damage of this type will be allowed, but if the damage is fatal, a loss will be triggered.</p>
*
* @return <p>The damage causes that won't be blocked</p>
*/
@NotNull
Set<EntityDamageEvent.DamageCause> getAllowedDamageCauses();
/**
* Sets the damage causes that will trigger a loss for this arena
*
* <p>Receiving any damage of this type will immediately cause a loss to the player.</p>
*
* @return <p>The damage causes that will trigger a loss</p>
*/
@NotNull
Set<EntityDamageEvent.DamageCause> getLossTriggerDamageCauses();
/**
* Sets the damage causes that are allowed for this arena
*
* @param causes <p>The allowed damage causes</p>
*/
@SuppressWarnings("SameReturnValue")
boolean setAllowedDamageCauses(@NotNull Set<EntityDamageEvent.DamageCause> causes);
/**
* Sets the damage causes that will trigger a loss for this arena
*
* @param causes <p>The causes that cause a loss</p>
*/
@SuppressWarnings("SameReturnValue")
boolean setLossTriggerDamageCauses(@NotNull Set<EntityDamageEvent.DamageCause> causes);
}

View File

@ -12,13 +12,15 @@ public interface ArenaGameMode {
*
* @return <p>The name of this game-mode</p>
*/
@NotNull String name();
@NotNull
String name();
/**
* Gets a set of all available arena game-modes in the type definition of this game-mode
*
* @return <p>All game-modes in this game-mode's class</p>
*/
@NotNull ArenaGameMode[] getValues();
@NotNull
ArenaGameMode[] getValues();
}

View File

@ -151,8 +151,12 @@ public abstract class ArenaHandler<K extends Arena, S extends ArenaGroup<K, S>>
* @return <p>The arena with the given name, or null if not found</p>
*/
public @Nullable K getArena(@NotNull String arenaName) {
try {
return this.arenas.get(UUID.fromString(arenaName));
} catch (IllegalArgumentException exception) {
return this.arenas.get(this.arenaNameLookup.get(StringSanitizer.sanitizeArenaName(arenaName)));
}
}
/**
* Gets all known arenas
@ -170,7 +174,7 @@ public abstract class ArenaHandler<K extends Arena, S extends ArenaGroup<K, S>>
*/
public void removeArena(@NotNull K arena) {
UUID arenaId = arena.getArenaId();
this.playerRegistry.removeForArena(arena);
this.playerRegistry.removeForArena(arena, false);
this.arenas.remove(arenaId);
this.arenaNameLookup.remove(arena.getArenaNameSanitized());
this.arenaGroups.remove(arenaId);

View File

@ -1,5 +1,11 @@
package net.knarcraft.minigames.arena;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.UUID;
/**
* A registry keeping track of all player sessions for some arenas
*
@ -7,11 +13,63 @@ package net.knarcraft.minigames.arena;
*/
public interface ArenaPlayerRegistry<K extends Arena> {
/**
* Gets the ids of the players currently playing
*
* @return <p>The ids of the playing players</p>
*/
@NotNull
Set<UUID> getPlayingPlayers();
/**
* Gets all players currently playing in the given arena
*
* @param arena <p>The arena to check</p>
* @return <p>All players currently in the arena</p>
*/
@NotNull
Set<UUID> getPlayingPlayers(@NotNull K arena);
/**
* Gets the current entry state for the given player
*
* @param playerId <p>The id of the player to get an entry state for</p>
* @return <p>The entry state of the player, or null if not found</p>
*/
@Nullable
PlayerEntryState getEntryState(@NotNull UUID playerId);
/**
* Registers that the given player has started playing the given dropper arena session
*
* @param playerId <p>The id of the player that started playing</p>
* @param arenaSession <p>The arena session to register</p>
*/
void registerPlayer(@NotNull UUID playerId, @NotNull ArenaSession arenaSession);
/**
* Removes this player from players currently playing
*
* @param playerId <p>The id of the player to remove</p>
* @param restoreState <p>Whether to restore the state of the player as part of the removal</p>
*/
boolean removePlayer(@NotNull UUID playerId, boolean restoreState);
/**
* Gets the player's active dropper arena session
*
* @param playerId <p>The id of the player to get arena for</p>
* @return <p>The player's active arena session, or null if not currently playing</p>
*/
@Nullable
ArenaSession getArenaSession(@NotNull UUID playerId);
/**
* Removes all active sessions for the given arena
*
* @param arena <p>The arena to remove sessions for</p>
* @param immediately <p>Whether to immediately teleport the player</p>
*/
void removeForArena(K arena);
void removeForArena(@NotNull K arena, boolean immediately);
}

View File

@ -1,6 +1,6 @@
package net.knarcraft.minigames.arena;
import org.bukkit.entity.Player;
import net.knarcraft.minigames.gui.ArenaGUI;
import org.jetbrains.annotations.NotNull;
/**
@ -8,19 +8,13 @@ import org.jetbrains.annotations.NotNull;
*/
public interface ArenaSession {
/**
* Gets the game-mode the player is playing in this session
*
* @return <p>The game-mode for this session</p>
*/
@NotNull ArenaGameMode getGameMode();
/**
* Gets the state of the player when they joined the session
*
* @return <p>The player's entry state</p>
*/
@NotNull PlayerEntryState getEntryState();
@NotNull
PlayerEntryState getEntryState();
/**
* Triggers a win for the player playing in this session
@ -36,21 +30,37 @@ public interface ArenaSession {
* Triggers a quit for the player playing in this session
*
* @param immediately <p>Whether to to the teleportation immediately, not using any timers</p>
* @param removeSession <p>Whether to also remove the session. Should usually be true.</p>
*/
void triggerQuit(boolean immediately);
void triggerQuit(boolean immediately, boolean removeSession);
/**
* Gets the arena this session is being played in
*
* @return <p>The session's arena</p>
*/
@NotNull Arena getArena();
@NotNull
Arena getArena();
/**
* Gets the player playing in this session
* Gets the GUI with this arena's options
*
* @return <p>This session's player</p>
* @return <p>This arena's GUI</p>
*/
@NotNull Player getPlayer();
@NotNull
ArenaGUI getGUI();
/**
* Resets the session to allow a player to try again
*/
void reset();
/**
* Gets the game-mode the player is playing
*
* @return <p>The game-mode the player is playing</p>
*/
@NotNull
ArenaGameMode getGameMode();
}

View File

@ -0,0 +1,58 @@
package net.knarcraft.minigames.arena;
/**
* The type of one editable property
*/
public enum EditablePropertyType {
/**
* The property is a location
*/
LOCATION,
/**
* The property is an arena name
*/
ARENA_NAME,
/**
* The property is a horizontal velocity
*/
HORIZONTAL_VELOCITY,
/**
* The property is a vertical velocity (fly speed)
*/
VERTICAL_VELOCITY,
/**
* The property is a material that specifies a block
*/
BLOCK_TYPE,
/**
* The property clears a checkpoint
*/
CHECKPOINT_CLEAR,
/**
* The property is a comma-separated list of materials
*/
MATERIAL_LIST,
/**
* The property is any double value
*/
DOUBLE,
/**
* The property is an integer with no particular restrictions
*/
INTEGER,
/**
* The property is a comma-separated list of damage causes
*/
DAMAGE_CAUSE_LIST,
}

View File

@ -1,11 +1,15 @@
package net.knarcraft.minigames.arena;
import org.bukkit.Location;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import java.util.UUID;
/**
* The stored state of a player
*/
public interface PlayerEntryState {
public interface PlayerEntryState extends ConfigurationSerializable {
/**
* Sets the state of the stored player to the state used by the arena
@ -15,7 +19,21 @@ public interface PlayerEntryState {
/**
* Restores the stored state for the stored player
*/
void restore();
boolean restore();
/**
* Restores the stored state for the given player
*
* @param player <p>A player object that's refers to the same player as the stored player</p>
*/
void restore(Player player);
/**
* Gets the id of the player this state belongs to
*
* @return <p>The player the state belongs to</p>
*/
UUID getPlayerId();
/**
* Gets the location the player entered from

View File

@ -0,0 +1,109 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* A manager for keeping track of which players have set other players as hidden
*/
public class PlayerVisibilityManager {
private final Set<UUID> displayingEnabledFor = new HashSet<>();
/**
* Toggles whether players should be hidden for the player with the given id
*
* @param player <p>The the player to update</p>
*/
public void toggleHidePlayers(@Nullable ArenaPlayerRegistry<?> playerRegistry, @NotNull Player player) {
if (displayingEnabledFor.contains(player.getUniqueId())) {
displayingEnabledFor.remove(player.getUniqueId());
// Make all other players hidden
changeVisibilityFor(playerRegistry, player, true);
} else {
displayingEnabledFor.add(player.getUniqueId());
// Make all other players visible again
changeVisibilityFor(playerRegistry, player, false);
}
}
/**
* Gets whether the given player is currently hiding other players
*
* @param player <p>The player to check</p>
* @return <p>True if currently hiding other players</p>
*/
public boolean isHidingPlayers(Player player) {
return !this.displayingEnabledFor.contains(player.getUniqueId());
}
/**
* Updates which players are seen as hidden
*
* @param playerRegistry <p>The registry containing all playing players</p>
* @param player <p>The player that joined the arena</p>
*/
public void updateHiddenPlayers(@NotNull ArenaPlayerRegistry<?> playerRegistry, @NotNull Player player) {
boolean hideForPlayer = !displayingEnabledFor.contains(player.getUniqueId());
for (UUID playerId : playerRegistry.getPlayingPlayers()) {
Player otherPlayer = Bukkit.getPlayer(playerId);
if (otherPlayer == null) {
continue;
}
// Hide the arena player from the newly joined player
if (hideForPlayer) {
player.hidePlayer(MiniGames.getInstance(), otherPlayer);
}
// Hide the newly joined player from this player
if (!displayingEnabledFor.contains(playerId)) {
otherPlayer.hidePlayer(MiniGames.getInstance(), player);
}
}
}
/**
* Makes all players visible to the given player
*
* @param player <p>The player to update visibility for</p>
*/
public void showPlayersFor(@NotNull Player player) {
for (Player otherPlayer : Bukkit.getOnlinePlayers()) {
player.showPlayer(MiniGames.getInstance(), otherPlayer);
otherPlayer.showPlayer(MiniGames.getInstance(), player);
}
}
/**
* Changes whether the given player can see the other players in the arena
*
* @param playerRegistry <p>The player registry containing other players</p>
* @param player <p>The player to change the visibility for</p>
* @param hide <p>Whether to hide the players or show the players</p>
*/
private void changeVisibilityFor(@Nullable ArenaPlayerRegistry<?> playerRegistry, @NotNull Player player, boolean hide) {
if (playerRegistry == null) {
return;
}
for (UUID playerId : playerRegistry.getPlayingPlayers()) {
Player otherPlayer = Bukkit.getPlayer(playerId);
if (otherPlayer == null) {
continue;
}
if (hide) {
player.hidePlayer(MiniGames.getInstance(), otherPlayer);
} else {
player.showPlayer(MiniGames.getInstance(), otherPlayer);
}
}
}
}

View File

@ -0,0 +1,18 @@
package net.knarcraft.minigames.arena;
import org.jetbrains.annotations.NotNull;
/**
* A representation of each key used for storing arena data
*/
public interface StorageKey {
/**
* Gets the configuration key this enum represents
*
* @return <p>The string key representation.</p>
*/
@NotNull
String getKey();
}

View File

@ -4,19 +4,26 @@ import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.reward.Reward;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.util.DropperArenaStorageHelper;
import net.knarcraft.minigames.util.InputValidationHelper;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
@ -58,6 +65,21 @@ public class DropperArena implements Arena {
*/
private float playerHorizontalVelocity;
/**
* The maximum amount of players able to join this arena at any time
*/
private int maxPlayers = -1;
/**
* Types of damage that won't be blocked in this arena
*/
private Set<EntityDamageEvent.DamageCause> allowedDamageCauses;
/**
* Types of damage that will trigger a loss in this arena
*/
private Set<EntityDamageEvent.DamageCause> lossTriggerDamageCauses;
/**
* The material of the block players have to hit to win this dropper arena
*/
@ -70,6 +92,8 @@ public class DropperArena implements Arena {
private final DropperArenaHandler dropperArenaHandler;
private Map<RewardCondition, Set<Reward>> rewards = new HashMap<>();
private static final DropperConfiguration dropperConfiguration = MiniGames.getInstance().getDropperConfiguration();
/**
@ -82,13 +106,20 @@ public class DropperArena implements Arena {
* @param playerVerticalVelocity <p>The velocity to use for players' vertical velocity</p>
* @param playerHorizontalVelocity <p>The velocity to use for players' horizontal velocity (-1 to 1)</p>
* @param winBlockType <p>The material of the block players have to hit to win this dropper arena</p>
* @param maxPlayers <p>The maximum amount of players able to join this arena at once</p>
* @param rewards <p>The rewards given by this arena</p>
* @param dropperArenaData <p>The arena data keeping track of which players have done what in this arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
* @param allowedDamageCauses <p>The damage causes to not cancel. If the player received fatal damage, a loss is triggered.</p>
* @param lossTriggerDamageCauses <p>The damage causes that will trigger a loss (for arrow traps and similar)</p>
*/
public DropperArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, double playerVerticalVelocity, float playerHorizontalVelocity,
@NotNull Material winBlockType, @NotNull DropperArenaData dropperArenaData,
@NotNull DropperArenaHandler arenaHandler) {
@NotNull Material winBlockType, int maxPlayers,
@NotNull Map<RewardCondition, Set<Reward>> rewards, @NotNull DropperArenaData dropperArenaData,
@NotNull DropperArenaHandler arenaHandler,
@Nullable Set<String> allowedDamageCauses,
@Nullable Set<String> lossTriggerDamageCauses) {
this.arenaId = arenaId;
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
@ -98,6 +129,10 @@ public class DropperArena implements Arena {
this.winBlockType = winBlockType;
this.dropperArenaData = dropperArenaData;
this.dropperArenaHandler = arenaHandler;
this.rewards = rewards;
this.maxPlayers = maxPlayers;
this.allowedDamageCauses = InputValidationHelper.parseDamageCauses(allowedDamageCauses);
this.lossTriggerDamageCauses = InputValidationHelper.parseDamageCauses(lossTriggerDamageCauses);
}
/**
@ -128,6 +163,8 @@ public class DropperArena implements Arena {
this.dropperArenaData = new DropperArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.WATER;
this.dropperArenaHandler = arenaHandler;
this.allowedDamageCauses = new HashSet<>();
this.lossTriggerDamageCauses = new HashSet<>();
}
@Override
@ -150,15 +187,151 @@ public class DropperArena implements Arena {
return this.spawnLocation.clone();
}
/**
* Gets this arena's exit location
*
* @return <p>This arena's exit location, or null if no such location is set.</p>
*/
@Override
public @Nullable Location getExitLocation() {
return this.exitLocation != null ? this.exitLocation.clone() : null;
}
@Override
public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) {
this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>());
this.rewards.get(rewardCondition).add(reward);
this.saveArena();
}
@Override
public void clearRewards(@NotNull RewardCondition rewardCondition) {
this.rewards.remove(rewardCondition);
this.saveArena();
}
@Override
public @NotNull Set<Reward> getRewards(RewardCondition rewardCondition) {
if (this.rewards.containsKey(rewardCondition) && this.rewards.get(rewardCondition) != null) {
return this.rewards.get(rewardCondition);
} else {
return new HashSet<>();
}
}
@Override
public int getMaxPlayers() {
return this.maxPlayers;
}
@Override
public boolean setMaxPlayers(int newValue) {
if (newValue < -1) {
return false;
}
this.maxPlayers = newValue;
this.saveArena();
return true;
}
@Override
public @NotNull Set<EntityDamageEvent.DamageCause> getAllowedDamageCauses() {
return this.allowedDamageCauses;
}
@Override
public @NotNull Set<EntityDamageEvent.DamageCause> getLossTriggerDamageCauses() {
return this.lossTriggerDamageCauses;
}
@Override
public boolean setAllowedDamageCauses(@NotNull Set<EntityDamageEvent.DamageCause> causes) {
this.allowedDamageCauses = causes;
this.saveArena();
return true;
}
@Override
public boolean setLossTriggerDamageCauses(@NotNull Set<EntityDamageEvent.DamageCause> causes) {
this.lossTriggerDamageCauses = causes;
this.saveArena();
return true;
}
@Override
@NotNull
public Material getWinBlockType() {
return this.winBlockType;
}
@Override
@NotNull
public String getArenaNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getArenaName());
}
@Override
public boolean removeData() {
return DropperArenaStorageHelper.removeDropperArenaData(getArenaId());
}
@Override
public boolean saveData() {
try {
DropperArenaStorageHelper.saveDropperArenaData(getData());
return true;
} catch (IOException exception) {
return false;
}
}
@Override
public boolean willCauseWin(@NotNull Block block) {
return block.getType() == winBlockType;
}
@Override
public boolean willCauseLoss(@NotNull Block block) {
return !dropperConfiguration.getBlockWhitelist().contains(block.getType());
}
@Override
public boolean winLocationIsSolid() {
return winBlockType.isSolid();
}
@Override
public boolean setSpawnLocation(@Nullable Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.spawnLocation = newLocation;
this.saveArena();
return true;
}
}
@Override
public boolean setExitLocation(@Nullable Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation;
this.saveArena();
return true;
}
}
@Override
public boolean setName(@NotNull String arenaName) {
if (!arenaName.isBlank()) {
String oldName = this.getArenaNameSanitized();
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
this.dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
this.saveArena();
return true;
} else {
return false;
}
}
/**
* Gets the vertical velocity for players in this arena
*
@ -182,119 +355,13 @@ public class DropperArena implements Arena {
return this.playerHorizontalVelocity;
}
/**
* Gets the type of block a player has to hit to win this arena
*
* @return <p>The kind of block players must hit</p>
*/
public @NotNull Material getWinBlockType() {
return this.winBlockType;
}
/**
* Gets this arena's sanitized name
*
* @return <p>This arena's sanitized name</p>
*/
public @NotNull String getArenaNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getArenaName());
}
@Override
public boolean removeData() {
return DropperArenaStorageHelper.removeDropperArenaData(getArenaId());
}
@Override
public boolean saveData() {
try {
DropperArenaStorageHelper.saveDropperArenaData(getData());
return true;
} catch (IOException e) {
return false;
}
}
@Override
public boolean willCauseWin(Block block) {
return block.getType() == winBlockType;
}
@Override
public boolean willCauseLoss(Block block) {
return !dropperConfiguration.getBlockWhitelist().contains(block.getType());
}
@Override
public boolean winLocationIsSolid() {
return winBlockType.isSolid();
}
/**
* Sets the spawn location for this arena
*
* @param newLocation <p>The new spawn location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setSpawnLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.spawnLocation = newLocation;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the exit location for this arena
*
* @param newLocation <p>The new exit location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setExitLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the name of this arena
*
* @param arenaName <p>The new name</p>
* @return <p>True if successfully updated</p>
*/
public boolean setName(@NotNull String arenaName) {
if (!arenaName.isBlank()) {
String oldName = this.getArenaNameSanitized();
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
dropperArenaHandler.saveArenas();
return true;
} else {
return false;
}
}
/**
* Sets the material of the win block type
*
* <p>The win block type is the type of block a player must hit to win in this arena</p>
*
* @param material <p>The material to set for the win block type</p>
* @return <p>True if successfully updated</p>
*/
public boolean setWinBlockType(@NotNull Material material) {
if (material.isAir() || !material.isBlock()) {
return false;
} else {
this.winBlockType = material;
dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -312,7 +379,7 @@ public class DropperArena implements Arena {
return false;
} else {
this.playerHorizontalVelocity = horizontalVelocity;
dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -328,11 +395,24 @@ public class DropperArena implements Arena {
return false;
} else {
this.playerVerticalVelocity = verticalVelocity;
dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
/**
* Saves this arena to disk
*/
public void saveArena() {
try {
DropperArenaStorageHelper.saveSingleDropperArena(this);
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save arena! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DropperArena otherArena)) {

View File

@ -1,5 +1,7 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.arena.EditablePropertyType;
import net.knarcraft.minigames.util.ArenaStorageHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -13,45 +15,82 @@ public enum DropperArenaEditableProperty {
/**
* The name of the arena
*/
NAME("name", DropperArena::getArenaName),
NAME("name", DropperArena::getArenaName, EditablePropertyType.ARENA_NAME),
/**
* The arena's spawn location
*/
SPAWN_LOCATION("spawnLocation", (arena) -> String.valueOf(arena.getSpawnLocation())),
SPAWN_LOCATION("spawnLocation", (arena) -> String.valueOf(arena.getSpawnLocation()),
EditablePropertyType.LOCATION),
/**
* The arena's exit location
*/
EXIT_LOCATION("exitLocation", (arena) -> String.valueOf(arena.getExitLocation())),
EXIT_LOCATION("exitLocation", (arena) -> String.valueOf(arena.getExitLocation()),
EditablePropertyType.LOCATION),
/**
* The arena's vertical velocity
*/
VERTICAL_VELOCITY("verticalVelocity", (arena) -> String.valueOf(arena.getPlayerVerticalVelocity())),
VERTICAL_VELOCITY("verticalVelocity", (arena) -> String.valueOf(arena.getPlayerVerticalVelocity()),
EditablePropertyType.VERTICAL_VELOCITY),
/**
* The arena's horizontal velocity
*/
HORIZONTAL_VELOCITY("horizontalVelocity", (arena) -> String.valueOf(arena.getPlayerHorizontalVelocity())),
HORIZONTAL_VELOCITY("horizontalVelocity", (arena) -> String.valueOf(arena.getPlayerHorizontalVelocity()),
EditablePropertyType.HORIZONTAL_VELOCITY),
/**
* The arena's win block type
*/
WIN_BLOCK_TYPE("winBlockType", (arena) -> arena.getWinBlockType().toString()),
WIN_BLOCK_TYPE("winBlockType", (arena) -> arena.getWinBlockType().toString(),
EditablePropertyType.BLOCK_TYPE),
/**
* The arena's max players
*/
MAX_PLAYERS("maxPlayers", (arena) -> String.valueOf(arena.getMaxPlayers()),
EditablePropertyType.INTEGER),
/**
* The arena's allowed damage causes
*/
ALLOWED_DAMAGE_CAUSES("allowedDamageCauses", (arena) -> String.valueOf(
ArenaStorageHelper.getDamageCauseNames(arena.getAllowedDamageCauses())),
EditablePropertyType.DAMAGE_CAUSE_LIST),
/**
* The arena's loss trigger damage causes
*/
LOSS_TRIGGER_DAMAGE_CAUSES("lossTriggerDamageCauses", (arena) -> String.valueOf(
ArenaStorageHelper.getDamageCauseNames(arena.getLossTriggerDamageCauses())),
EditablePropertyType.DAMAGE_CAUSE_LIST),
;
private final @NotNull String argumentString;
private final Function<DropperArena, String> currentValueProvider;
private final EditablePropertyType propertyType;
/**
* Instantiates a new arena editable property
*
* @param argumentString <p>The argument string used to specify this property</p>
*/
DropperArenaEditableProperty(@NotNull String argumentString, Function<DropperArena, String> currentValueProvider) {
DropperArenaEditableProperty(@NotNull String argumentString, Function<DropperArena, String> currentValueProvider,
EditablePropertyType propertyType) {
this.argumentString = argumentString;
this.currentValueProvider = currentValueProvider;
this.propertyType = propertyType;
}
/**
* Gets the type of property this editable property represents
*
* @return <p>The type of this property</p>
*/
public EditablePropertyType getPropertyType() {
return this.propertyType;
}
/**

View File

@ -34,11 +34,11 @@ public enum DropperArenaGameMode implements ConfigurationSerializable, ArenaGame
* @param gameMode <p>The game-mode string to match</p>
* @return <p>The specified arena game-mode</p>
*/
public static @NotNull DropperArenaGameMode matchGamemode(@NotNull String gameMode) {
public static @NotNull DropperArenaGameMode matchGameMode(@NotNull String gameMode) {
String sanitized = gameMode.trim().toLowerCase();
if (sanitized.matches("(invert(ed)?|inverse)")) {
return DropperArenaGameMode.INVERTED;
} else if (sanitized.matches("rand(om)?")) {
} else if (sanitized.matches("rand(om)?_?(inverted)?")) {
return DropperArenaGameMode.RANDOM_INVERTED;
} else {
return DropperArenaGameMode.DEFAULT;

View File

@ -2,6 +2,8 @@ package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaHandler;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.DropperArenaStorageHelper;
import java.io.IOException;
@ -22,7 +24,7 @@ public class DropperArenaHandler extends ArenaHandler<DropperArena, DropperArena
*
* @param playerRegistry <p>The registry keeping track of player sessions</p>
*/
public DropperArenaHandler(DropperArenaPlayerRegistry playerRegistry) {
public DropperArenaHandler(ArenaPlayerRegistry<DropperArena> playerRegistry) {
super(playerRegistry);
}
@ -30,10 +32,10 @@ public class DropperArenaHandler extends ArenaHandler<DropperArena, DropperArena
public void saveGroups() {
try {
DropperArenaStorageHelper.saveDropperArenaGroups(new HashSet<>(this.arenaGroups.values()));
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, MiniGames.getInstance().getTranslator().getTranslatedMessage(
MiniGameMessage.ERROR_CANNOT_SAVE_ARENA_GROUPS));
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}
@ -53,10 +55,10 @@ public class DropperArenaHandler extends ArenaHandler<DropperArena, DropperArena
public void saveArenas() {
try {
DropperArenaStorageHelper.saveDropperArenas(this.arenas);
} catch (IOException e) {
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save current arenas! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}

View File

@ -1,62 +1,15 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.knarcraft.minigames.arena.AbstractArenaPlayerRegistry;
/**
* A registry to keep track of which players are playing in which arenas
*/
public class DropperArenaPlayerRegistry implements ArenaPlayerRegistry<DropperArena> {
public class DropperArenaPlayerRegistry extends AbstractArenaPlayerRegistry<DropperArena> {
private final Map<UUID, DropperArenaSession> arenaPlayers = new HashMap<>();
/**
* Registers that the given player has started playing the given dropper arena session
*
* @param playerId <p>The id of the player that started playing</p>
* @param arena <p>The arena session to register</p>
*/
public void registerPlayer(@NotNull UUID playerId, @NotNull DropperArenaSession arena) {
this.arenaPlayers.put(playerId, arena);
}
/**
* Removes this player from players currently playing
*
* @param playerId <p>The id of the player to remove</p>
*/
public boolean removePlayer(@NotNull UUID playerId) {
return this.arenaPlayers.remove(playerId) != null;
}
/**
* Gets the player's active dropper arena session
*
* @param playerId <p>The id of the player to get arena for</p>
* @return <p>The player's active arena session, or null if not currently playing</p>
*/
public @Nullable DropperArenaSession getArenaSession(@NotNull UUID playerId) {
return this.arenaPlayers.getOrDefault(playerId, null);
}
/**
* Removes all active sessions for the given arena
*
* @param arena <p>The arena to remove sessions for</p>
*/
public void removeForArena(DropperArena arena) {
for (Map.Entry<UUID, DropperArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit(false);
this.arenaPlayers.remove(entry.getKey());
}
}
@Override
protected String getEntryStateStorageKey() {
return "dropper";
}
}

View File

@ -1,13 +1,18 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.AbstractArenaSession;
import net.knarcraft.minigames.arena.PlayerEntryState;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.property.RecordResult;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.gui.ArenaGUI;
import net.knarcraft.minigames.gui.DropperGUI;
import net.knarcraft.minigames.gui.DropperGUIBedrock;
import net.knarcraft.minigames.util.GeyserHelper;
import net.knarcraft.minigames.util.PlayerTeleporter;
import org.bukkit.Location;
import net.knarcraft.minigames.util.RewardHelper;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@ -16,14 +21,12 @@ import java.util.logging.Level;
/**
* A representation of a player's current session in a dropper arena
*/
public class DropperArenaSession implements ArenaSession {
public class DropperArenaSession extends AbstractArenaSession {
private final @NotNull DropperArena arena;
private final @NotNull Player player;
private final @NotNull DropperArenaGameMode gameMode;
private int deaths;
private final long startTime;
private final PlayerEntryState entryState;
private boolean startedMoving = false;
/**
* Instantiates a new dropper arena session
@ -34,171 +37,29 @@ public class DropperArenaSession implements ArenaSession {
*/
public DropperArenaSession(@NotNull DropperArena dropperArena, @NotNull Player player,
@NotNull DropperArenaGameMode gameMode) {
super(dropperArena, player, gameMode);
this.arena = dropperArena;
this.player = player;
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
DropperConfiguration configuration = MiniGames.getInstance().getDropperConfiguration();
boolean makeInvisible = configuration.makePlayersInvisible();
boolean disableCollision = configuration.disableHitCollision();
this.entryState = new DropperPlayerEntryState(player, gameMode, makeInvisible, disableCollision,
dropperArena.getPlayerHorizontalVelocity());
// Make the player fly to improve mobility in the air
this.entryState = new DropperPlayerEntryState(player, gameMode, dropperArena.getPlayerHorizontalVelocity());
this.entryState.setArenaState();
}
/**
* Gets the game-mode the player is playing in this session
* Marks that this arena's player has started moving
*/
public void setStartedMoving() {
this.startedMoving = true;
}
/**
* Gets whether the player of this session has started moving in the arena
*
* @return <p>The game-mode for this session</p>
* @return <p>True if the player has started moving</p>
*/
public @NotNull DropperArenaGameMode getGameMode() {
return this.gameMode;
}
/**
* Gets the state of the player when they joined the session
*
* @return <p>The player's entry state</p>
*/
public @NotNull PlayerEntryState getEntryState() {
return this.entryState;
}
/**
* Triggers a win for the player playing in this session
*/
public void triggerWin() {
// Stop this session
stopSession();
// Check for, and display, records
MiniGames miniGames = MiniGames.getInstance();
boolean ignore = miniGames.getDropperConfiguration().ignoreRecordsUntilGroupBeatenOnce();
DropperArenaGroup group = miniGames.getDropperArenaHandler().getGroup(this.arena.getArenaId());
if (!ignore || group == null || group.hasBeatenAll(this.gameMode, this.player)) {
registerRecord();
}
// Mark the arena as cleared
if (this.arena.getData().setCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
}
this.player.sendMessage("You won!");
// Teleport the player out of the arena
teleportToExit(false);
}
/**
* Teleports the playing player out of the arena
*
* @param immediately <p>Whether to to the teleportation immediately, not using any timers</p>
*/
private void teleportToExit(boolean immediately) {
// Teleport the player out of the arena
Location exitLocation;
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
* Removes this session from current sessions
*/
private void removeSession() {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().removePlayer(player.getUniqueId());
if (!removedSession) {
MiniGames.log(Level.SEVERE, "Unable to remove dropper arena session for " + player.getName() + ". " +
"This will have unintended consequences.");
}
}
/**
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
ArenaRecordsRegistry recordsRegistry = this.arena.getData().getRecordRegistries().get(this.gameMode);
long timeElapsed = System.currentTimeMillis() - this.startTime;
announceRecord(recordsRegistry.registerTimeRecord(this.player.getUniqueId(), timeElapsed), "time");
announceRecord(recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths), "least deaths");
}
/**
* Announces a record set by this player
*
* @param recordResult <p>The result of the record</p>
* @param type <p>The type of record set (time or deaths)</p>
*/
private void announceRecord(@NotNull RecordResult recordResult, @NotNull String type) {
if (recordResult == RecordResult.NONE) {
return;
}
// Gets a string representation of the played game-mode
String gameModeString = switch (this.gameMode) {
case DEFAULT -> "default";
case INVERTED -> "inverted";
case RANDOM_INVERTED -> "random";
};
String recordString = "You just set a %s on the %s game-mode!";
recordString = switch (recordResult) {
case WORLD_RECORD -> String.format(recordString, "new %s record", gameModeString);
case PERSONAL_BEST -> String.format(recordString, "personal %s record", gameModeString);
default -> throw new IllegalStateException("Unexpected value: " + recordResult);
};
player.sendMessage(String.format(recordString, type));
}
/**
* Triggers a loss for the player playing in this session
*/
public void triggerLoss() {
this.deaths++;
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true, false);
this.entryState.setArenaState();
}
/**
* Triggers a quit for the player playing in this session
*
* @param immediately <p>Whether to to the teleportation immediately, not using any timers</p>
*/
public void triggerQuit(boolean immediately) {
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit(immediately);
player.sendMessage("You quit the arena!");
}
/**
* Stops this session, and disables flight mode
*/
private void stopSession() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// Remove flight mode
entryState.restore();
}
/**
* Gets the arena this session is being played in
*
* @return <p>The session's arena</p>
*/
public @NotNull DropperArena getArena() {
return this.arena;
public boolean getStartedMoving() {
return this.startedMoving;
}
/**
@ -210,4 +71,94 @@ public class DropperArenaSession implements ArenaSession {
return this.player;
}
/**
* Gets the game-mode the player is playing in this session
*
* @return <p>The game-mode for this session</p>
*/
public @NotNull DropperArenaGameMode getGameMode() {
return this.gameMode;
}
@Override
public @NotNull PlayerEntryState getEntryState() {
return this.entryState;
}
@Override
public void triggerWin() {
// Stop this session
removeSession();
// Check for, and display, records
MiniGames miniGames = MiniGames.getInstance();
boolean ignore = miniGames.getDropperConfiguration().ignoreRecordsUntilGroupBeatenOnce();
DropperArenaGroup group = miniGames.getDropperArenaHandler().getGroup(this.arena.getArenaId());
if (!ignore || group == null || group.hasBeatenAll(this.gameMode, this.player)) {
registerRecord();
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
// Mark the arena as cleared
if (this.arena.getData().setCompleted(this.gameMode, this.player)) {
stringFormatter.displaySuccessMessage(this.player, MiniGameMessage.SUCCESS_ARENA_FIRST_CLEAR);
RewardHelper.grantRewards(this.player, this.arena.getRewards(RewardCondition.FIRST_WIN));
}
stringFormatter.displaySuccessMessage(this.player, MiniGameMessage.SUCCESS_ARENA_WIN);
RewardHelper.grantRewards(this.player, this.arena.getRewards(RewardCondition.WIN));
// Teleport the player out of the arena
teleportToExit(false);
}
@Override
public void triggerLoss() {
this.deaths++;
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true, false);
this.player.playSound(this.player, Sound.ENTITY_CHICKEN_EGG, 5f, 0.5f);
this.entryState.setArenaState();
}
@Override
public @NotNull DropperArena getArena() {
return this.arena;
}
@Override
public @NotNull ArenaGUI getGUI() {
if (GeyserHelper.isGeyserPlayer(this.player)) {
return new DropperGUIBedrock(this.player);
} else {
return new DropperGUI(this.player);
}
}
@Override
public void reset() {
this.startedMoving = false;
super.reset();
}
@Override
protected void removeSession() {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().removePlayer(
this.player.getUniqueId(), true);
if (!removedSession) {
MiniGames.log(Level.SEVERE, "Unable to remove dropper arena session for " + player.getName() + ". " +
"This will have unintended consequences.");
}
}
@Override
protected String getGameModeString() {
return switch (this.gameMode) {
case DEFAULT -> "default";
case INVERTED -> "inverted";
case RANDOM_INVERTED -> "random";
};
}
}

View File

@ -1,11 +1,12 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.arena.StorageKey;
import org.jetbrains.annotations.NotNull;
/**
* A representation of each key used for storing arena data
*/
public enum DropperArenaStorageKey {
public enum DropperArenaStorageKey implements StorageKey {
/**
* The key for an arena's id
@ -43,9 +44,29 @@ public enum DropperArenaStorageKey {
WIN_BLOCK_TYPE("winBlockType"),
/**
* The hey for this arena's data
* The key for this arena's data
*/
DATA("arenaData"),
/**
* The key for this arena's rewards
*/
REWARDS("rewards"),
/**
* The key for this arena's maximum players
*/
MAX_PLAYERS("maxPlayers"),
/**
* The key for this arena's allowed damage causes
*/
ALLOWED_DAMAGE_CAUSES("allowedDamageCauses"),
/**
* The key for this arena's loss trigger damage causes
*/
LOSS_TRIGGER_DAMAGE_CAUSES("lossTriggerDamageCauses"),
;
private final @NotNull String key;
@ -59,11 +80,7 @@ public enum DropperArenaStorageKey {
this.key = key;
}
/**
* Gets the configuration key this enum represents
*
* @return <p>The string key representation.</p>
*/
@Override
public @NotNull String getKey() {
return this.key;
}

View File

@ -1,17 +1,24 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.arena.AbstractPlayerEntryState;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
/**
* The state of a player before entering a dropper arena
*/
public class DropperPlayerEntryState extends AbstractPlayerEntryState {
private final float originalFlySpeed;
private final boolean disableHitCollision;
private final float horizontalVelocity;
private final DropperArenaGameMode arenaGameMode;
@ -21,37 +28,117 @@ public class DropperPlayerEntryState extends AbstractPlayerEntryState {
* @param player <p>The player whose state should be stored</p>
*/
public DropperPlayerEntryState(@NotNull Player player, @NotNull DropperArenaGameMode arenaGameMode,
boolean makePlayerInvisible, boolean disableHitCollision, float horizontalVelocity) {
super(player, makePlayerInvisible);
float horizontalVelocity) {
super(player);
this.originalFlySpeed = player.getFlySpeed();
this.arenaGameMode = arenaGameMode;
this.disableHitCollision = disableHitCollision;
this.horizontalVelocity = horizontalVelocity;
}
/**
* Instantiates a new parkour player entry state
*
* @param playerId <p>The id of the player whose state this should keep track of</p>
* @param entryLocation <p>The location the player entered from</p>
* @param originalIsFlying <p>Whether the player was flying before entering the arena</p>
* @param originalGameMode <p>The game-mode of the player before entering the arena</p>
* @param originalAllowFlight <p>Whether the player was allowed flight before entering the arena</p>
* @param originalInvulnerable <p>Whether the player was invulnerable before entering the arena</p>
* @param originalIsSwimming <p>Whether the player was swimming before entering the arena</p>
* @param originalFlySpeed <p>The fly-speed of the player before entering the arena</p>
* @param horizontalVelocity <p>The horizontal velocity of the player before entering the arena</p>
* @param originalCollideAble <p>Whether the player was collide-able before entering the arena</p>
* @param originalPotionEffects <p>The potion effects applied to the player when joining</p>
* @param originalHealth <p>The health of the player when joining the arena</p>
* @param originalSaturation <p>The saturation of the player when joining the arena</p>
*/
public DropperPlayerEntryState(@NotNull UUID playerId, @NotNull Location entryLocation,
boolean originalIsFlying, GameMode originalGameMode, boolean originalAllowFlight,
boolean originalInvulnerable, boolean originalIsSwimming,
float originalFlySpeed, float horizontalVelocity,
@NotNull DropperArenaGameMode arenaGameMode, boolean originalCollideAble,
@NotNull Collection<PotionEffect> originalPotionEffects, double originalHealth,
float originalSaturation) {
super(playerId, entryLocation, originalIsFlying, originalGameMode, originalAllowFlight,
originalInvulnerable, originalIsSwimming, originalCollideAble, originalPotionEffects, originalHealth,
originalSaturation);
this.originalFlySpeed = originalFlySpeed;
this.horizontalVelocity = horizontalVelocity;
this.arenaGameMode = arenaGameMode;
}
@Override
public void setArenaState() {
super.setArenaState();
this.player.setAllowFlight(true);
this.player.setFlying(true);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
if (this.disableHitCollision) {
this.player.setCollidable(false);
Player player = getPlayer();
if (player == null) {
return;
}
player.setAllowFlight(true);
player.setFlying(true);
player.setGameMode(GameMode.ADVENTURE);
player.setSwimming(false);
// If playing on the inverted game-mode, negate the horizontal velocity to swap the controls
if (this.arenaGameMode == DropperArenaGameMode.INVERTED) {
this.player.setFlySpeed(-this.horizontalVelocity);
player.setFlySpeed(-this.horizontalVelocity);
} else {
this.player.setFlySpeed(this.horizontalVelocity);
player.setFlySpeed(this.horizontalVelocity);
}
}
@Override
public void restore() {
super.restore();
this.player.setFlySpeed(this.originalFlySpeed);
public boolean restore() {
Player player = getPlayer();
if (player == null) {
return false;
}
this.restore(player);
return true;
}
@Override
public void restore(@NotNull Player player) {
super.restore(player);
player.setFlySpeed(this.originalFlySpeed);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = super.serialize();
data.put("originalFlySpeed", this.originalFlySpeed);
data.put("horizontalVelocity", this.horizontalVelocity);
data.put("arenaGameMode", this.arenaGameMode);
return data;
}
/**
* Deserializes a ParkourPlayerEntryState from the given data
*
* @return <p>The data to deserialize</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static DropperPlayerEntryState deserialize(Map<String, Object> data) {
UUID playerId = ((SerializableUUID) data.get("playerId")).getRawValue();
Location entryLocation = (Location) data.get("entryLocation");
boolean originalIsFlying = getBoolean(data, "originalIsFlying");
GameMode originalGameMode = GameMode.valueOf((String) data.get("originalGameMode"));
boolean originalAllowFlight = getBoolean(data, "originalAllowFlight");
boolean originalInvulnerable = getBoolean(data, "originalInvulnerable");
boolean originalIsSwimming = getBoolean(data, "originalIsSwimming");
float originalFlySpeed = ((Number) data.get("originalFlySpeed")).floatValue();
float horizontalVelocity = ((Number) data.get("horizontalVelocity")).floatValue();
DropperArenaGameMode arenaGameMode = (DropperArenaGameMode) data.get("arenaGameMode");
boolean originalCollideAble = getBoolean(data, "originalCollideAble");
Collection<PotionEffect> originalPotionEffect =
(Collection<PotionEffect>) data.getOrDefault("originalPotionEffects", new ArrayList<>());
double originalHealth = ((Number) data.get("originalHealth")).doubleValue();
float originalSaturation = ((Number) data.get("originalSaturation")).floatValue();
return new DropperPlayerEntryState(playerId, entryLocation, originalIsFlying,
originalGameMode, originalAllowFlight, originalInvulnerable, originalIsSwimming,
originalFlySpeed, horizontalVelocity, arenaGameMode, originalCollideAble, originalPotionEffect,
originalHealth, originalSaturation);
}
}

View File

@ -1,15 +1,20 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.knarlib.util.MaterialHelper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.util.MaterialHelper;
import net.knarcraft.minigames.arena.reward.Reward;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.util.InputValidationHelper;
import net.knarcraft.minigames.util.ParkourArenaStorageHelper;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -21,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
@ -70,6 +76,31 @@ public class ParkourArena implements Arena {
*/
private @Nullable Set<Material> killPlaneBlocks;
/**
* The names of the block types serving as obstacles for this arena
*/
private @Nullable Set<String> obstacleBlockNames;
/**
* The block types serving as obstacles for this arena
*/
private @Nullable Set<Material> obstacleBlocks;
/**
* The maximum amount of players able to join this arena at any time
*/
private int maxPlayers;
/**
* Types of damage that won't be blocked in this arena
*/
private Set<EntityDamageEvent.DamageCause> allowedDamageCauses;
/**
* Types of damage that will trigger a loss in this arena
*/
private Set<EntityDamageEvent.DamageCause> lossTriggerDamageCauses;
/**
* The checkpoints for this arena. Entering a checkpoint overrides the player's spawn location.
*/
@ -82,6 +113,8 @@ public class ParkourArena implements Arena {
private final @NotNull ParkourArenaHandler parkourArenaHandler;
private Map<RewardCondition, Set<Reward>> rewards = new HashMap<>();
/**
* Instantiates a new parkour arena
*
@ -91,13 +124,24 @@ public class ParkourArena implements Arena {
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param winBlockType <p>The material of the block players have to hit to win this parkour arena</p>
* @param winLocation <p>The location a player has to reach to win this arena</p>
* @param killPlaneBlockNames <p>The names of the types of blocks that trigger a loss when stepped on</p>
* @param obstacleBlockNames <p>The names of the types of blocks that trigger a loss when touched</p>
* @param checkpoints <p>The checkpoints set for this arena</p>
* @param maxPlayers <p>The maximum amount of players able to join this arena at once</p>
* @param rewards <p>The rewards given by this arena</p>
* @param parkourArenaData <p>The arena data keeping track of which players have done what in this arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
* @param allowedDamageCauses <p>The damage causes to not cancel. If the player received fatal damage, a loss is triggered.</p>
* @param lossTriggerDamageCauses <p>The damage causes that will trigger a loss (for arrow traps and similar)</p>
*/
public ParkourArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, @NotNull Material winBlockType, @Nullable Location winLocation,
@Nullable Set<String> killPlaneBlockNames, @NotNull List<Location> checkpoints,
@NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler) {
@Nullable Set<String> killPlaneBlockNames, @Nullable Set<String> obstacleBlockNames,
@NotNull List<Location> checkpoints, int maxPlayers,
@NotNull Map<RewardCondition, Set<Reward>> rewards,
@NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler,
@Nullable Set<String> allowedDamageCauses,
@Nullable Set<String> lossTriggerDamageCauses) {
this.arenaId = arenaId;
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
@ -106,10 +150,17 @@ public class ParkourArena implements Arena {
this.winLocation = winLocation;
this.killPlaneBlockNames = killPlaneBlockNames;
this.killPlaneBlocks = this.killPlaneBlockNames == null ? null : MaterialHelper.loadMaterialList(
new ArrayList<>(killPlaneBlockNames));
new ArrayList<>(killPlaneBlockNames), "+", MiniGames.getInstance().getLogger());
this.obstacleBlockNames = obstacleBlockNames;
this.obstacleBlocks = this.obstacleBlockNames == null ? null : MaterialHelper.loadMaterialList(
new ArrayList<>(obstacleBlockNames), "+", MiniGames.getInstance().getLogger());
this.checkpoints = checkpoints;
this.parkourArenaData = parkourArenaData;
this.parkourArenaHandler = arenaHandler;
this.rewards = rewards;
this.maxPlayers = maxPlayers;
this.allowedDamageCauses = InputValidationHelper.parseDamageCauses(allowedDamageCauses);
this.lossTriggerDamageCauses = InputValidationHelper.parseDamageCauses(lossTriggerDamageCauses);
}
/**
@ -138,8 +189,12 @@ public class ParkourArena implements Arena {
this.parkourArenaData = new ParkourArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.EMERALD_BLOCK;
this.killPlaneBlocks = null;
this.obstacleBlocks = null;
this.checkpoints = new ArrayList<>();
this.parkourArenaHandler = arenaHandler;
this.maxPlayers = -1;
this.allowedDamageCauses = new HashSet<>();
this.lossTriggerDamageCauses = new HashSet<>();
}
@Override
@ -162,21 +217,76 @@ public class ParkourArena implements Arena {
return this.spawnLocation;
}
/**
* Gets this arena's exit location
*
* @return <p>This arena's exit location, or null if no such location is set.</p>
*/
@Override
public @Nullable Location getExitLocation() {
return this.exitLocation;
}
/**
* Gets the type of block a player has to hit to win this arena
*
* @return <p>The kind of block players must hit</p>
*/
public @NotNull Material getWinBlockType() {
@Override
public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) {
this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>());
this.rewards.get(rewardCondition).add(reward);
this.saveArena();
}
@Override
public void clearRewards(@NotNull RewardCondition rewardCondition) {
this.rewards.remove(rewardCondition);
this.saveArena();
}
@Override
public @NotNull Set<Reward> getRewards(RewardCondition rewardCondition) {
if (this.rewards.containsKey(rewardCondition)) {
return this.rewards.get(rewardCondition);
} else {
return new HashSet<>();
}
}
@Override
public int getMaxPlayers() {
return this.maxPlayers;
}
@Override
public boolean setMaxPlayers(int newValue) {
if (newValue < -1) {
return false;
}
this.maxPlayers = newValue;
this.saveArena();
return true;
}
@Override
public @NotNull Set<EntityDamageEvent.DamageCause> getAllowedDamageCauses() {
return this.allowedDamageCauses;
}
@Override
public @NotNull Set<EntityDamageEvent.DamageCause> getLossTriggerDamageCauses() {
return this.lossTriggerDamageCauses;
}
@Override
public boolean setAllowedDamageCauses(@NotNull Set<EntityDamageEvent.DamageCause> causes) {
this.allowedDamageCauses = causes;
this.saveArena();
return true;
}
@Override
public boolean setLossTriggerDamageCauses(@NotNull Set<EntityDamageEvent.DamageCause> causes) {
this.lossTriggerDamageCauses = causes;
this.saveArena();
return true;
}
@Override
@NotNull
public Material getWinBlockType() {
return this.winBlockType;
}
@ -213,6 +323,28 @@ public class ParkourArena implements Arena {
return this.killPlaneBlockNames;
}
/**
* Gets the block types used for this parkour arena's obstacle blocks
*
* @return <p>The types of blocks used as obstacles</p>
*/
public @NotNull Set<Material> getObstacleBlocks() {
if (this.obstacleBlocks != null) {
return new HashSet<>(this.obstacleBlocks);
} else {
return MiniGames.getInstance().getParkourConfiguration().getObstacleBlocks();
}
}
/**
* Gets the names of the blocks used as this arena's obstacle blocks
*
* @return <p>The names of the blocks used as this arena's obstacle blocks</p>
*/
public @Nullable Set<String> getObstacleBlockNames() {
return this.obstacleBlockNames;
}
/**
* Gets all checkpoint locations for this arena
*
@ -227,11 +359,17 @@ public class ParkourArena implements Arena {
}
/**
* Gets this arena's sanitized name
* Gets whether this arena has no checkpoints
*
* @return <p>This arena's sanitized name</p>
* @return <p>True if this arena has no checkpoints</p>
*/
public @NotNull String getArenaNameSanitized() {
public boolean hasNoCheckpoints() {
return this.checkpoints.isEmpty();
}
@Override
@NotNull
public String getArenaNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getArenaName());
}
@ -245,20 +383,27 @@ public class ParkourArena implements Arena {
try {
ParkourArenaStorageHelper.saveParkourArenaData(getData());
return true;
} catch (IOException e) {
} catch (IOException exception) {
return false;
}
}
@Override
public boolean willCauseWin(Block block) {
return (this.winLocation != null && this.winLocation.getBlock().equals(block)) ||
(this.winLocation == null && this.winBlockType == block.getType());
public boolean willCauseWin(@NotNull Block block) {
if (this.winLocation != null) {
return this.winLocation.getBlock().equals(block);
} else {
if (this.winBlockType.isSolid()) {
return this.winBlockType == block.getRelative(BlockFace.DOWN).getType();
} else {
return this.winBlockType == block.getType();
}
}
}
@Override
public boolean willCauseLoss(Block block) {
return this.getKillPlaneBlocks().contains(block.getType());
public boolean willCauseLoss(@NotNull Block block) {
return this.getKillPlaneBlocks().contains(block.getType()) || this.getObstacleBlocks().contains(block.getType());
}
@Override
@ -267,71 +412,49 @@ public class ParkourArena implements Arena {
this.winBlockType.isSolid();
}
/**
* Sets the spawn location for this arena
*
* @param newLocation <p>The new spawn location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setSpawnLocation(@NotNull Location newLocation) {
@Override
public boolean setSpawnLocation(@Nullable Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.spawnLocation = newLocation;
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
/**
* Sets the exit location for this arena
*
* @param newLocation <p>The new exit location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setExitLocation(@NotNull Location newLocation) {
@Override
public boolean setExitLocation(@Nullable Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation;
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
/**
* Sets the name of this arena
*
* @param arenaName <p>The new name</p>
* @return <p>True if successfully updated</p>
*/
@Override
public boolean setName(@NotNull String arenaName) {
if (!arenaName.isBlank()) {
String oldName = this.getArenaNameSanitized();
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
this.parkourArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
} else {
return false;
}
}
/**
* Sets the material of the win block type
*
* <p>The win block type is the type of block a player must hit to win in this arena</p>
*
* @param material <p>The material to set for the win block type</p>
* @return <p>True if successfully updated</p>
*/
@Override
public boolean setWinBlockType(@NotNull Material material) {
if (material.isAir() || !material.isBlock()) {
return false;
} else {
this.winBlockType = material;
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -342,13 +465,18 @@ public class ParkourArena implements Arena {
* @param newLocation <p>The location players have to reach</p>
* @return <p>True if successfully changed</p>
*/
public boolean setWinLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.winLocation = newLocation.clone();
this.parkourArenaHandler.saveArenas();
public boolean setWinLocation(@Nullable Location newLocation) {
if (newLocation == null) {
// Un-set the win location, and fall back to the win block type
this.winLocation = null;
this.saveArena();
return true;
} else if (!isInvalid(newLocation)) {
this.winLocation = newLocation.clone();
this.saveArena();
return true;
} else {
return false;
}
}
@ -357,17 +485,42 @@ public class ParkourArena implements Arena {
*
* @param killPlaneBlockNames <p>The names of the blocks that will cause players to lose</p>
*/
public boolean setKillPlaneBlocks(@NotNull Set<String> killPlaneBlockNames) {
if (killPlaneBlockNames.isEmpty()) {
public boolean setKillPlaneBlocks(@Nullable Set<String> killPlaneBlockNames) {
if (killPlaneBlockNames == null || killPlaneBlockNames.isEmpty()) {
this.killPlaneBlockNames = null;
this.killPlaneBlocks = null;
} else {
Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(killPlaneBlockNames));
Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(killPlaneBlockNames), "+",
MiniGames.getInstance().getLogger());
if (parsed.isEmpty()) {
return false;
}
this.killPlaneBlockNames = killPlaneBlockNames;
this.killPlaneBlocks = parsed;
}
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
/**
* Sets the type of blocks used as obstacle blocks
*
* @param obstacleBlockNames <p>The names of the obstacle blocks</p>
*/
public boolean setObstacleBlocks(@Nullable Set<String> obstacleBlockNames) {
if (obstacleBlockNames == null || obstacleBlockNames.isEmpty()) {
this.obstacleBlockNames = null;
this.obstacleBlocks = null;
} else {
Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(obstacleBlockNames), "+",
MiniGames.getInstance().getLogger());
if (parsed.isEmpty()) {
return false;
}
this.obstacleBlockNames = obstacleBlockNames;
this.obstacleBlocks = parsed;
}
this.saveArena();
return true;
}
@ -377,13 +530,13 @@ public class ParkourArena implements Arena {
* @param checkpoint <p>The checkpoint to add</p>
* @return <p>True if successfully added</p>
*/
public boolean addCheckpoint(@NotNull Location checkpoint) {
public boolean addCheckpoint(@Nullable Location checkpoint) {
if (isInvalid(checkpoint)) {
return false;
}
this.checkpoints.add(checkpoint.clone());
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
@ -398,10 +551,23 @@ public class ParkourArena implements Arena {
}
this.checkpoints.clear();
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
/**
* Saves this arena to disk
*/
public void saveArena() {
try {
ParkourArenaStorageHelper.saveSingleParkourArena(this);
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save arena! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof ParkourArena otherArena)) {

View File

@ -1,5 +1,7 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.arena.EditablePropertyType;
import net.knarcraft.minigames.util.ArenaStorageHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -13,22 +15,25 @@ public enum ParkourArenaEditableProperty {
/**
* The name of the arena
*/
NAME("name", ParkourArena::getArenaName),
NAME("name", ParkourArena::getArenaName, EditablePropertyType.ARENA_NAME),
/**
* The arena's spawn location
*/
SPAWN_LOCATION("spawnLocation", (arena) -> String.valueOf(arena.getSpawnLocation())),
SPAWN_LOCATION("spawnLocation", (arena) -> String.valueOf(arena.getSpawnLocation()),
EditablePropertyType.LOCATION),
/**
* The arena's exit location
*/
EXIT_LOCATION("exitLocation", (arena) -> String.valueOf(arena.getExitLocation())),
EXIT_LOCATION("exitLocation", (arena) -> String.valueOf(arena.getExitLocation()),
EditablePropertyType.LOCATION),
/**
* The arena's win block type
*/
WIN_BLOCK_TYPE("winBlockType", (arena) -> arena.getWinBlockType().toString()),
WIN_BLOCK_TYPE("winBlockType", (arena) -> arena.getWinBlockType().toString(),
EditablePropertyType.BLOCK_TYPE),
/**
* The arena's win location (overrides the win block type)
@ -39,35 +44,76 @@ public enum ParkourArenaEditableProperty {
} else {
return "null";
}
}),
}, EditablePropertyType.LOCATION),
/**
* The arena's check points. Specifically used for adding.
*/
CHECKPOINT_ADD("checkpointAdd", (arena) -> String.valueOf(arena.getCheckpoints())),
CHECKPOINT_ADD("checkpointAdd", (arena) -> String.valueOf(arena.getCheckpoints()),
EditablePropertyType.LOCATION),
/**
* The arena's check points. Specifically used for clearing.
*/
CHECKPOINT_CLEAR("checkpointClear", (arena) -> String.valueOf(arena.getCheckpoints())),
CHECKPOINT_CLEAR("checkpointClear", (arena) -> String.valueOf(arena.getCheckpoints()),
EditablePropertyType.CHECKPOINT_CLEAR),
/**
* The blocks constituting the arena's lethal blocks
*/
KILL_PLANE_BLOCKS("killPlaneBlocks", (arena) -> String.valueOf(arena.getKillPlaneBlockNames())),
KILL_PLANE_BLOCKS("killPlaneBlocks", (arena) -> String.valueOf(arena.getKillPlaneBlockNames()),
EditablePropertyType.MATERIAL_LIST),
/**
* The blocks used as this arena's obstacle blocks
*/
OBSTACLE_BLOCKS("obstacleBlocks", (arena) -> String.valueOf(arena.getObstacleBlockNames()),
EditablePropertyType.MATERIAL_LIST),
/**
* The arena's max players
*/
MAX_PLAYERS("maxPlayers", (arena) -> String.valueOf(arena.getMaxPlayers()),
EditablePropertyType.INTEGER),
/**
* The arena's allowed damage causes
*/
ALLOWED_DAMAGE_CAUSES("allowedDamageCauses", (arena) -> String.valueOf(
ArenaStorageHelper.getDamageCauseNames(arena.getAllowedDamageCauses())),
EditablePropertyType.DAMAGE_CAUSE_LIST),
/**
* The arena's loss trigger damage causes
*/
LOSS_TRIGGER_DAMAGE_CAUSES("lossTriggerDamageCauses", (arena) -> String.valueOf(
ArenaStorageHelper.getDamageCauseNames(arena.getLossTriggerDamageCauses())),
EditablePropertyType.DAMAGE_CAUSE_LIST),
;
private final @NotNull String argumentString;
private final Function<ParkourArena, String> currentValueProvider;
private final EditablePropertyType propertyType;
/**
* Instantiates a new arena editable property
*
* @param argumentString <p>The argument string used to specify this property</p>
*/
ParkourArenaEditableProperty(@NotNull String argumentString, Function<ParkourArena, String> currentValueProvider) {
ParkourArenaEditableProperty(@NotNull String argumentString, Function<ParkourArena, String> currentValueProvider,
EditablePropertyType propertyType) {
this.argumentString = argumentString;
this.currentValueProvider = currentValueProvider;
this.propertyType = propertyType;
}
/**
* Gets the type of property this editable property represents
*
* @return <p>The type of this property</p>
*/
public EditablePropertyType getPropertyType() {
return this.propertyType;
}
/**

View File

@ -16,6 +16,11 @@ public enum ParkourArenaGameMode implements ConfigurationSerializable, ArenaGame
* The default game-mode. Failing once throws the player out.
*/
DEFAULT,
/**
* A hardcore game mode where no checkpoints are allowed
*/
HARDCORE,
;
/**
@ -24,7 +29,7 @@ public enum ParkourArenaGameMode implements ConfigurationSerializable, ArenaGame
* @param gameMode <p>The game-mode string to match</p>
* @return <p>The specified arena game-mode</p>
*/
public static @NotNull ParkourArenaGameMode matchGamemode(@NotNull String gameMode) {
public static @NotNull ParkourArenaGameMode matchGameMode(@NotNull String gameMode) {
try {
return ParkourArenaGameMode.valueOf(gameMode.toUpperCase());
} catch (IllegalArgumentException exception) {

View File

@ -2,6 +2,8 @@ package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaHandler;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.ParkourArenaStorageHelper;
import java.io.IOException;
@ -22,7 +24,7 @@ public class ParkourArenaHandler extends ArenaHandler<ParkourArena, ParkourArena
*
* @param playerRegistry <p>The registry keeping track of player sessions</p>
*/
public ParkourArenaHandler(ParkourArenaPlayerRegistry playerRegistry) {
public ParkourArenaHandler(ArenaPlayerRegistry<ParkourArena> playerRegistry) {
super(playerRegistry);
}
@ -30,10 +32,10 @@ public class ParkourArenaHandler extends ArenaHandler<ParkourArena, ParkourArena
public void saveGroups() {
try {
ParkourArenaStorageHelper.saveParkourArenaGroups(new HashSet<>(this.arenaGroups.values()));
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, MiniGames.getInstance().getTranslator().getTranslatedMessage(
MiniGameMessage.ERROR_CANNOT_SAVE_ARENA_GROUPS));
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}
@ -53,10 +55,10 @@ public class ParkourArenaHandler extends ArenaHandler<ParkourArena, ParkourArena
public void saveArenas() {
try {
ParkourArenaStorageHelper.saveParkourArenas(this.arenas);
} catch (IOException e) {
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save current arenas! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}

View File

@ -1,62 +1,15 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.knarcraft.minigames.arena.AbstractArenaPlayerRegistry;
/**
* A registry to keep track of which players are playing in which arenas
*/
public class ParkourArenaPlayerRegistry implements ArenaPlayerRegistry<ParkourArena> {
public class ParkourArenaPlayerRegistry extends AbstractArenaPlayerRegistry<ParkourArena> {
private final Map<UUID, ParkourArenaSession> arenaPlayers = new HashMap<>();
/**
* Registers that the given player has started playing the given parkour arena session
*
* @param playerId <p>The id of the player that started playing</p>
* @param arena <p>The arena session to register</p>
*/
public void registerPlayer(@NotNull UUID playerId, @NotNull ParkourArenaSession arena) {
this.arenaPlayers.put(playerId, arena);
}
/**
* Removes this player from players currently playing
*
* @param playerId <p>The id of the player to remove</p>
*/
public boolean removePlayer(@NotNull UUID playerId) {
return this.arenaPlayers.remove(playerId) != null;
}
/**
* Gets the player's active parkour arena session
*
* @param playerId <p>The id of the player to get arena for</p>
* @return <p>The player's active arena session, or null if not currently playing</p>
*/
public @Nullable ParkourArenaSession getArenaSession(@NotNull UUID playerId) {
return this.arenaPlayers.getOrDefault(playerId, null);
}
/**
* Removes all active sessions for the given arena
*
* @param arena <p>The arena to remove sessions for</p>
*/
public void removeForArena(ParkourArena arena) {
for (Map.Entry<UUID, ParkourArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit(false);
this.arenaPlayers.remove(entry.getKey());
}
}
@Override
protected String getEntryStateStorageKey() {
return "parkour";
}
}

View File

@ -1,32 +1,43 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.AbstractArenaSession;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.PlayerEntryState;
import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.property.RecordResult;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.gui.ArenaGUI;
import net.knarcraft.minigames.gui.ParkourGUI;
import net.knarcraft.minigames.gui.ParkourGUIBedrock;
import net.knarcraft.minigames.util.GeyserHelper;
import net.knarcraft.minigames.util.PlayerTeleporter;
import net.knarcraft.minigames.util.RewardHelper;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Powerable;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
/**
* A representation of a player's current session in a parkour arena
*/
public class ParkourArenaSession implements ArenaSession {
public class ParkourArenaSession extends AbstractArenaSession {
private static final @NotNull Map<Arena, Set<Block>> changedLevers = new HashMap<>();
private final @NotNull ParkourArena arena;
private final @NotNull Player player;
private final @NotNull ParkourArenaGameMode gameMode;
private int deaths;
private final long startTime;
private final PlayerEntryState entryState;
private Location reachedCheckpoint = null;
private @Nullable Location reachedCheckpoint = null;
/**
* Instantiates a new parkour arena session
@ -37,31 +48,22 @@ public class ParkourArenaSession implements ArenaSession {
*/
public ParkourArenaSession(@NotNull ParkourArena parkourArena, @NotNull Player player,
@NotNull ParkourArenaGameMode gameMode) {
super(parkourArena, player, gameMode);
this.arena = parkourArena;
this.player = player;
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
ParkourConfiguration configuration = MiniGames.getInstance().getParkourConfiguration();
boolean makeInvisible = configuration.makePlayersInvisible();
this.entryState = new ParkourPlayerEntryState(player, makeInvisible);
// Make the player fly to improve mobility in the air
this.entryState = new ParkourPlayerEntryState(player);
this.entryState.setArenaState();
}
@Override
public @NotNull ArenaGameMode getGameMode() {
return this.gameMode;
}
/**
* Gets the state of the player when they joined the session
* Gets the game-mode the player is playing in this session
*
* @return <p>The player's entry state</p>
* @return <p>The game-mode for this session</p>
*/
public @NotNull PlayerEntryState getEntryState() {
return this.entryState;
public @NotNull ParkourArenaGameMode getGameMode() {
return this.gameMode;
}
/**
@ -73,6 +75,16 @@ public class ParkourArenaSession implements ArenaSession {
this.reachedCheckpoint = location;
}
/**
* Registers a lever change
*
* @param block <p>The block of the lever</p>
*/
public void registerChangedLever(@NotNull Block block) {
changedLevers.putIfAbsent(this.arena, new HashSet<>());
changedLevers.get(this.arena).add(block);
}
/**
* Gets the checkpoint currently registered as the player's spawn location
*
@ -82,12 +94,15 @@ public class ParkourArenaSession implements ArenaSession {
return this.reachedCheckpoint;
}
/**
* Triggers a win for the player playing in this session
*/
@Override
public @NotNull PlayerEntryState getEntryState() {
return this.entryState;
}
@Override
public void triggerWin() {
// Stop this session
stopSession();
removeSession();
// Check for, and display, records
MiniGames miniGames = MiniGames.getInstance();
@ -97,127 +112,104 @@ public class ParkourArenaSession implements ArenaSession {
registerRecord();
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
// Mark the arena as cleared
if (this.arena.getData().setCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
stringFormatter.displaySuccessMessage(this.player, MiniGameMessage.SUCCESS_ARENA_FIRST_CLEAR);
RewardHelper.grantRewards(this.player, this.arena.getRewards(RewardCondition.FIRST_WIN));
}
this.player.sendMessage("You won!");
stringFormatter.displaySuccessMessage(this.player, MiniGameMessage.SUCCESS_ARENA_WIN);
RewardHelper.grantRewards(this.player, this.arena.getRewards(RewardCondition.WIN));
// Teleport the player out of the arena
teleportToExit(false);
}
/**
* Teleports the playing player out of the arena
*/
private void teleportToExit(boolean immediately) {
// Teleport the player out of the arena
Location exitLocation;
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
* Removes this session from current sessions
*/
private void removeSession() {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = MiniGames.getInstance().getParkourArenaPlayerRegistry().removePlayer(player.getUniqueId());
if (!removedSession) {
MiniGames.log(Level.SEVERE, "Unable to remove parkour arena session for " + player.getName() + ". " +
"This will have unintended consequences.");
if (MiniGames.getInstance().getParkourArenaPlayerRegistry().getPlayingPlayers(this.arena).isEmpty()) {
resetLevers();
}
}
/**
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
ArenaRecordsRegistry recordsRegistry = this.arena.getData().getRecordRegistries().get(this.gameMode);
long timeElapsed = System.currentTimeMillis() - this.startTime;
announceRecord(recordsRegistry.registerTimeRecord(this.player.getUniqueId(), timeElapsed), "time");
announceRecord(recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths), "least deaths");
}
/**
* Announces a record set by this player
*
* @param recordResult <p>The result of the record</p>
* @param type <p>The type of record set (time or deaths)</p>
*/
private void announceRecord(@NotNull RecordResult recordResult, @NotNull String type) {
if (recordResult == RecordResult.NONE) {
return;
}
// Gets a string representation of the played game-mode
String gameModeString = switch (this.gameMode) {
case DEFAULT -> "default";
};
String recordString = "You just set a %s on the %s game-mode!";
recordString = switch (recordResult) {
case WORLD_RECORD -> String.format(recordString, "new %s record", gameModeString);
case PERSONAL_BEST -> String.format(recordString, "personal %s record", gameModeString);
default -> throw new IllegalStateException("Unexpected value: " + recordResult);
};
player.sendMessage(String.format(recordString, type));
}
/**
* Triggers a loss for the player playing in this session
*/
@Override
public void triggerLoss() {
this.deaths++;
//Teleport the player back to the top
Location spawnLocation = this.reachedCheckpoint != null ? this.reachedCheckpoint : this.arena.getSpawnLocation();
PlayerTeleporter.teleportPlayer(this.player, spawnLocation, true, false);
this.player.playSound(this.player, Sound.ENTITY_CHICKEN_EGG, 5f, 0.5f);
this.entryState.setArenaState();
}
/**
* Triggers a quit for the player playing in this session
*/
public void triggerQuit(boolean immediately) {
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit(immediately);
player.sendMessage("You quit the arena!");
}
/**
* Stops this session, and disables flight mode
*/
private void stopSession() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// Remove flight mode
entryState.restore();
}
/**
* Gets the arena this session is being played in
*
* @return <p>The session's arena</p>
*/
@Override
public @NotNull ParkourArena getArena() {
return this.arena;
}
@Override
public @NotNull ArenaGUI getGUI() {
if (GeyserHelper.isGeyserPlayer(this.player)) {
return new ParkourGUIBedrock(this.player);
} else {
return new ParkourGUI(this.player);
}
}
@Override
public void reset() {
this.reachedCheckpoint = null;
if (MiniGames.getInstance().getParkourArenaPlayerRegistry().getPlayingPlayers(this.arena).size() == 1) {
resetLevers();
}
super.reset();
}
@Override
protected void removeSession() {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = MiniGames.getInstance().getParkourArenaPlayerRegistry().removePlayer(
player.getUniqueId(), true);
if (!removedSession) {
MiniGames.log(Level.SEVERE, "Unable to remove parkour arena session for " + this.player.getName() +
". This will have unintended consequences.");
}
}
@Override
protected String getGameModeString() {
return switch (this.gameMode) {
case DEFAULT -> "default";
case HARDCORE -> "hardcore";
};
}
@Override
public void triggerQuit(boolean immediately, boolean removeSession) {
super.triggerQuit(immediately, removeSession);
// Note: As immediately is only used when stopping the server, levers should be reset regardless of current players
if (MiniGames.getInstance().getParkourArenaPlayerRegistry().getPlayingPlayers(this.arena).isEmpty() || immediately) {
resetLevers();
}
}
/**
* Gets the player playing in this session
*
* @return <p>This session's player</p>
* Resets all levers if the arena is empty
*/
public @NotNull Player getPlayer() {
return this.player;
private void resetLevers() {
// Make a copy in case new player join while the levers are resetting
Set<Block> changed = changedLevers.remove(this.arena);
if (changed == null || changed.isEmpty()) {
return;
}
// Reset all levers toggled by players in the arena
for (Block block : changed) {
BlockData blockData = block.getBlockData();
if (blockData instanceof Powerable powerable) {
powerable.setPowered(false);
block.setBlockData(blockData);
}
}
}
}

View File

@ -1,11 +1,12 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.arena.StorageKey;
import org.jetbrains.annotations.NotNull;
/**
* A representation of each key used for storing arena data
*/
public enum ParkourArenaStorageKey {
public enum ParkourArenaStorageKey implements StorageKey {
/**
* The key for an arena's id
@ -42,6 +43,11 @@ public enum ParkourArenaStorageKey {
*/
KILL_PLANE_BLOCKS("killPlaneBlocks"),
/**
* The key for this arena's obstacle blocks (overrides the config)
*/
OBSTACLE_BLOCKS("obstacleBlocks"),
/**
* The key for this arena's checkpoint locations
*/
@ -51,6 +57,26 @@ public enum ParkourArenaStorageKey {
* The hey for this arena's data
*/
DATA("arenaData"),
/**
* The key for this arena's rewards
*/
REWARDS("rewards"),
/**
* The key for this arena's maximum players
*/
MAX_PLAYERS("maxPlayers"),
/**
* The key for this arena's allowed damage causes
*/
ALLOWED_DAMAGE_CAUSES("allowedDamageCauses"),
/**
* The key for this arena's loss trigger damage causes
*/
LOSS_TRIGGER_DAMAGE_CAUSES("lossTriggerDamageCauses"),
;
private final @NotNull String key;
@ -64,11 +90,7 @@ public enum ParkourArenaStorageKey {
this.key = key;
}
/**
* Gets the configuration key this enum represents
*
* @return <p>The string key representation.</p>
*/
@Override
public @NotNull String getKey() {
return this.key;
}

View File

@ -1,10 +1,18 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.arena.AbstractPlayerEntryState;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
/**
* The state of a player before entering a parkour arena
*/
@ -15,18 +23,70 @@ public class ParkourPlayerEntryState extends AbstractPlayerEntryState {
*
* @param player <p>The player whose state should be stored</p>
*/
public ParkourPlayerEntryState(@NotNull Player player, boolean makePlayerInvisible) {
super(player, makePlayerInvisible);
public ParkourPlayerEntryState(@NotNull Player player) {
super(player);
}
/**
* Instantiates a new parkour player entry state
*
* @param playerId <p>The id of the player whose state this should keep track of</p>
* @param entryLocation <p>The location the player entered from</p>
* @param originalIsFlying <p>Whether the player was flying before entering the arena</p>
* @param originalGameMode <p>The game-mode of the player before entering the arena</p>
* @param originalAllowFlight <p>Whether the player was allowed flight before entering the arena</p>
* @param originalInvulnerable <p>Whether the player was invulnerable before entering the arena</p>
* @param originalIsSwimming <p>Whether the player was swimming before entering the arena</p>
* @param originalCollideAble <p>Whether the player was collide-able before entering the arena</p>
* @param originalPotionEffects <p>The potion effects applied to the player when joining</p>
* @param originalHealth <p>The health of the player when joining the arena</p>
* @param originalSaturation <p>The saturation of the player when joining the arena</p>
*/
public ParkourPlayerEntryState(@NotNull UUID playerId, Location entryLocation,
boolean originalIsFlying, GameMode originalGameMode, boolean originalAllowFlight,
boolean originalInvulnerable, boolean originalIsSwimming,
boolean originalCollideAble, Collection<PotionEffect> originalPotionEffects,
double originalHealth, float originalSaturation) {
super(playerId, entryLocation, originalIsFlying, originalGameMode, originalAllowFlight,
originalInvulnerable, originalIsSwimming, originalCollideAble, originalPotionEffects, originalHealth,
originalSaturation);
}
@Override
public void setArenaState() {
super.setArenaState();
this.player.setAllowFlight(false);
this.player.setFlying(false);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
this.player.setCollidable(false);
Player player = getPlayer();
if (player == null) {
return;
}
player.setAllowFlight(false);
player.setFlying(false);
player.setGameMode(GameMode.ADVENTURE);
player.setSwimming(false);
}
/**
* Deserializes a ParkourPlayerEntryState from the given data
*
* @return <p>The data to deserialize</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static ParkourPlayerEntryState deserialize(Map<String, Object> data) {
UUID playerId = ((SerializableUUID) data.get("playerId")).getRawValue();
Location entryLocation = (Location) data.get("entryLocation");
boolean originalIsFlying = getBoolean(data, "originalIsFlying");
GameMode originalGameMode = GameMode.valueOf((String) data.get("originalGameMode"));
boolean originalAllowFlight = getBoolean(data, "originalAllowFlight");
boolean originalInvulnerable = getBoolean(data, "originalInvulnerable");
boolean originalIsSwimming = getBoolean(data, "originalIsSwimming");
boolean originalCollideAble = getBoolean(data, "originalCollideAble");
Collection<PotionEffect> originalPotionEffect =
(Collection<PotionEffect>) data.getOrDefault("originalPotionEffects", new ArrayList<>());
double originalHealth = ((Number) data.get("originalHealth")).doubleValue();
float originalSaturation = ((Number) data.get("originalSaturation")).floatValue();
return new ParkourPlayerEntryState(playerId, entryLocation, originalIsFlying, originalGameMode,
originalAllowFlight, originalInvulnerable, originalIsSwimming, originalCollideAble,
originalPotionEffect, originalHealth, originalSaturation);
}
}

View File

@ -44,6 +44,13 @@ public abstract class ArenaRecord<K extends Comparable<K>> implements Comparable
return record;
}
/**
* Gets this as a string that should be printed on a sign
*
* @return <p>This as a string</p>
*/
public abstract String getAsString();
@Override
public boolean equals(Object other) {
return other instanceof ArenaRecord<?> && userId.equals(((ArenaRecord<?>) other).userId);

View File

@ -19,6 +19,11 @@ public class IntegerRecord extends SummableArenaRecord<Integer> {
super(userId, record);
}
@Override
public String getAsString() {
return String.valueOf(this.getRecord());
}
@Override
public SummableArenaRecord<Integer> sum(Integer value) {
return new IntegerRecord(this.getUserId(), this.getRecord() + value);

View File

@ -7,7 +7,7 @@ import java.util.Map;
import java.util.UUID;
/**
* A record storing a Long
* A record storing a Long time
*/
public class LongRecord extends SummableArenaRecord<Long> {
@ -29,6 +29,22 @@ public class LongRecord extends SummableArenaRecord<Long> {
return new LongRecord(this.getUserId(), this.getRecord() + value);
}
@Override
public String getAsString() {
double seconds = getRecord() / 1000.0;
double minutes = 0;
if (seconds > 60) {
minutes = seconds / 60.0;
seconds = seconds % 60;
}
if (minutes > 0) {
return String.format("%.0fm%.2fs", minutes, seconds);
} else {
return String.format("%.2fs", seconds);
}
}
/**
* Deserializes the saved arena record
*
@ -37,7 +53,8 @@ public class LongRecord extends SummableArenaRecord<Long> {
*/
@SuppressWarnings("unused")
public static LongRecord deserialize(@NotNull Map<String, Object> data) {
return new LongRecord(((SerializableUUID) data.get("userId")).getRawValue(), ((Number) data.get("record")).longValue());
return new LongRecord(((SerializableUUID) data.get("userId")).getRawValue(),
((Number) data.get("record")).longValue());
}
}

View File

@ -0,0 +1,69 @@
package net.knarcraft.minigames.arena.reward;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A reward that executes a specified command when it's granted
*/
public class CommandReward implements Reward {
private final @NotNull String command;
/**
* Instantiates a new command reward
*
* @param command <p>The command to execute when granting this reward</p>
*/
public CommandReward(@NotNull String command) {
this.command = command;
}
@Override
public boolean grant(@NotNull Player player) {
return Bukkit.dispatchCommand(Bukkit.getServer().getConsoleSender(), replaceNamePlaceholder(player, command));
}
@Override
public @NotNull String getGrantMessage() {
return MiniGames.getInstance().getStringFormatter().replacePlaceholder(
MiniGameMessage.SUCCESS_COMMAND_REWARDED, "{command}", command);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("command", command);
return data;
}
/**
* Replaces the name placeholder in the given input with the given player's name
*
* @param player <p>The player whose name should be used</p>
* @param input <p>The input containing a name placeholder</p>
* @return <p>The input with the placeholder replaced</p>
*/
private String replaceNamePlaceholder(@NotNull Player player, @NotNull String input) {
return input.replaceAll("[<%(\\[{]player[_\\-]?(name)?[>%)\\]}]", player.getName());
}
/**
* Deserializes the command reward defined in the given data
*
* @param data <p>The data to deserialize from</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static CommandReward deserialize(Map<String, Object> data) {
return new CommandReward((String) data.get("command"));
}
}

View File

@ -0,0 +1,65 @@
package net.knarcraft.minigames.arena.reward;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.manager.EconomyManager;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* A reward that gives an amount of currency when it's granted
*/
public class EconomyReward implements Reward {
private final double amount;
/**
* Instantiates a new economy reward
*
* @param amount <p>The amount of currency granted</p>
*/
public EconomyReward(double amount) {
this.amount = amount;
}
@Override
public boolean grant(@NotNull Player player) {
if (!EconomyManager.isInitialized()) {
MiniGames.log(Level.SEVERE, "An economy reward has been set, but no Vault-compatible economy" +
" plugin has been initialized.");
return false;
}
EconomyManager.deposit(player, amount);
return true;
}
@Override
public @NotNull String getGrantMessage() {
return MiniGames.getInstance().getStringFormatter().replacePlaceholder(MiniGameMessage.SUCCESS_ECONOMY_REWARDED,
"{currency}", EconomyManager.format(amount));
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("amount", amount);
return data;
}
/**
* Deserializes the economy reward defined in the given data
*
* @param data <p>The data to deserialize from</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static EconomyReward deserialize(Map<String, Object> data) {
return new EconomyReward((Double) data.get("amount"));
}
}

View File

@ -0,0 +1,110 @@
package net.knarcraft.minigames.arena.reward;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A reward that gives an item stack when granted
*/
public class ItemReward implements Reward {
private final ItemStack item;
/**
* Instantiates a new item reward
*
* @param item <p>The item rewarded</p>
*/
public ItemReward(@NotNull ItemStack item) {
if (item.getAmount() > item.getMaxStackSize()) {
throw new IllegalArgumentException("Item stack exceeds the maximum stack size");
}
this.item = item;
}
@Override
public boolean grant(@NotNull Player player) {
Inventory inventory = player.getInventory();
if (canFitItem(inventory)) {
inventory.addItem(item);
return true;
} else {
return false;
}
}
@Override
public @NotNull String getGrantMessage() {
NamespacedKey key = item.getType().getKeyOrNull();
String name = key == null ? "Unnamed item" : key.getKey().replace("_", " ");
return MiniGames.getInstance().getStringFormatter().replacePlaceholders(MiniGameMessage.SUCCESS_ITEM_REWARDED,
new String[]{"{amount}", "{item}"}, new String[]{String.valueOf(item.getAmount()), name});
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("item", item);
return data;
}
/**
* Deserializes the item reward defined in the given data
*
* @param data <p>The data to deserialize from</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static ItemReward deserialize(Map<String, Object> data) {
return new ItemReward((ItemStack) data.get("item"));
}
/**
* Checks whether the given inventory is able to fit this item reward
*
* @param inventory <p>The inventory to check</p>
* @return <p>True if the inventory can fit the item</p>
*/
private boolean canFitItem(Inventory inventory) {
// If a slot is available, there is no problem
if (inventory.firstEmpty() != -1) {
return true;
}
// If the inventory doesn't contain the correct type of item, stacking is impossible
if (!inventory.contains(item.getType())) {
return false;
}
// Check if the item stack can fit in the inventory if stacked with existing items
int availableSlots = 0;
for (ItemStack itemStack : inventory.getStorageContents()) {
ItemMeta itemMeta = itemStack.getItemMeta();
ItemMeta targetMeta = item.getItemMeta();
// Skip items of a different type, or with metadata that would prevent stacking
if (itemStack.getType() != item.getType() ||
(itemMeta != null && targetMeta != null && !itemMeta.equals(targetMeta))) {
continue;
}
availableSlots += itemStack.getMaxStackSize() - itemStack.getAmount();
if (availableSlots < item.getAmount()) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,85 @@
package net.knarcraft.minigames.arena.reward;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.manager.PermissionManager;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* A reward that grants a specified permission when granted
*/
public class PermissionReward implements Reward {
private final @NotNull String permission;
private final @Nullable World world;
/**
* Instantiates a new permission reward
*
* @param world <p>The world to grant the permission for, or null</p>
* @param permission <p>The permission to grant</p>
*/
public PermissionReward(@Nullable World world, @NotNull String permission) {
this.world = world;
this.permission = permission;
}
@Override
public boolean grant(@NotNull Player player) {
if (!PermissionManager.isInitialized()) {
MiniGames.log(Level.SEVERE, "A permission reward has been set, but no Vault-compatible permission" +
" plugin has been initialized.");
return false;
}
if (PermissionManager.hasPermission(player, this.permission, this.world != null ? this.world.getName() : null)) {
return false;
} else {
PermissionManager.addPermission(player, this.permission, this.world);
return true;
}
}
@Override
public @NotNull String getGrantMessage() {
if (world == null) {
return MiniGames.getInstance().getStringFormatter().replacePlaceholder(
MiniGameMessage.SUCCESS_PERMISSION_REWARDED, "{permission}", permission);
} else {
return MiniGames.getInstance().getStringFormatter().replacePlaceholders(
MiniGameMessage.SUCCESS_PERMISSION_REWARDED_WORLD, new String[]{"{permission}", "{world}"},
new String[]{permission, world.getName()});
}
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
if (world != null) {
data.put("world", world);
}
data.put("permission", permission);
return data;
}
/**
* Deserializes the permission reward defined in the given data
*
* @param data <p>The data to deserialize from</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static PermissionReward deserialize(Map<String, Object> data) {
World world = (World) data.getOrDefault("world", null);
String permission = (String) data.get("permission");
return new PermissionReward(world, permission);
}
}

View File

@ -0,0 +1,28 @@
package net.knarcraft.minigames.arena.reward;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* A reward a player can be awarded
*/
public interface Reward extends ConfigurationSerializable {
/**
* Grants this reward to the given player
*
* @param player <p>The player this reward should be granted to</p>
* @return <p>True if the item was granted. False if not possible.</p>
*/
boolean grant(@NotNull Player player);
/**
* Gets the message to display to a user when granting this reward
*
* @return <p>The message to display when this reward is granted</p>
*/
@NotNull
String getGrantMessage();
}

View File

@ -0,0 +1,83 @@
package net.knarcraft.minigames.arena.reward;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* The condition for granting a reward
*/
public enum RewardCondition implements ConfigurationSerializable {
/**
* The reward is granted each time the player wins/clears the arena
*/
WIN,
/**
* The reward is granted the first time the player wins/clears the arena
*/
FIRST_WIN,
/**
* The reward is granted if the player beats their personal least deaths record
*/
PERSONAL_DEATH_RECORD,
/**
* The reward is granted if the player beats their personal least time record
*/
PERSONAL_TIME_RECORD,
/**
* The reward is granted if the player beats the global least deaths record
*/
GLOBAL_DEATH_RECORD,
/**
* The reward is granted if the player beats the global least time record
*/
GLOBAL_TIME_RECORD,
;
/**
* Gets a reward condition from the given string
*
* @param condition <p>The string specifying a reward condition</p>
* @return <p>The matching reward condition, or null if not found</p>
*/
public static @Nullable RewardCondition getFromString(@NotNull String condition) {
for (RewardCondition rewardCondition : RewardCondition.values()) {
if (rewardCondition.name().equalsIgnoreCase(condition.replace("-", "_"))) {
return rewardCondition;
}
}
return null;
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("condition", this.name());
return data;
}
/**
* Deserializes a reward condition from the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized reward condition</p>
*/
@SuppressWarnings({"unused"})
public static @NotNull RewardCondition deserialize(@NotNull Map<String, Object> data) {
RewardCondition rewardCondition = getFromString(String.valueOf(data.get("condition")));
return Objects.requireNonNullElse(rewardCondition, RewardCondition.FIRST_WIN);
}
}

View File

@ -0,0 +1,47 @@
package net.knarcraft.minigames.arena.reward;
import org.jetbrains.annotations.NotNull;
/**
* The type of a specific reward
*/
public enum RewardType {
/**
* A command reward
*/
COMMAND,
/**
* An economy reward
*/
ECONOMY,
/**
* An item reward
*/
ITEM,
/**
* A permission reward
*/
PERMISSION,
;
/**
* Gets a reward type from the given string
*
* @param condition <p>The string specifying a reward type</p>
* @return <p>The matching reward type, or null if not found</p>
*/
public static RewardType getFromString(@NotNull String condition) {
for (RewardType rewardType : RewardType.values()) {
if (rewardType.name().equalsIgnoreCase(condition.replace("-", "_"))) {
return rewardType;
}
}
return null;
}
}

View File

@ -0,0 +1,134 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.util.InputValidationHelper;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.CommandExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An abstract arena edit command, implementing input validation
*/
public abstract class EditArenaCommand implements CommandExecutor {
private final DropperConfiguration configuration;
/**
* Instantiates a new edit arena command
*
* @param configuration <p>The configuration to use</p>
*/
public EditArenaCommand(DropperConfiguration configuration) {
this.configuration = configuration;
}
/**
* Parses the specified max players
*
* @param maxPlayers <p>The max players string to parse</p>
* @return <p>The parsed value, or -1 if not parse-able</p>
*/
protected int parseMaxPlayers(@NotNull String maxPlayers) {
try {
return Integer.parseInt(maxPlayers);
} catch (NumberFormatException exception) {
return -1;
}
}
/**
* Sanitizes the player's specified vertical velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 0.5 if not parse-able</p>
*/
protected double sanitizeVerticalVelocity(@NotNull String velocityString) {
// Vertical velocity should not be negative, as it would make the player go upwards. There is technically not a
// max speed limit, but setting it too high makes the arena unplayable
double velocity;
try {
velocity = Double.parseDouble(velocityString);
} catch (NumberFormatException exception) {
velocity = configuration.getVerticalVelocity();
}
// Require at least speed of 0.001, and at most 75 blocks/s
return Math.min(Math.max(velocity, 0.001), 75);
}
/**
* Sanitizes the user's specified horizontal velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 1 if not parse-able</p>
*/
protected float sanitizeHorizontalVelocity(@NotNull String velocityString) {
// Horizontal velocity is valid between -1 and 1, where negative values swaps directions
float velocity;
try {
velocity = Float.parseFloat(velocityString);
} catch (NumberFormatException exception) {
velocity = configuration.getHorizontalVelocity();
}
// If outside bonds, choose the most extreme value
return Math.min(Math.max(0.1f, velocity), 1);
}
/**
* Parses the given location string
*
* @param player <p>The player changing a location</p>
* @param locationString <p>The location string to parse</p>
* @return <p>The parsed location, or the player's location if not parse-able</p>
*/
protected @NotNull Location parseLocation(Player player, String locationString) {
if ((locationString.trim() + ",").matches("([0-9]+.?[0-9]*,){3}")) {
String[] parts = locationString.split(",");
Location newLocation = player.getLocation().clone();
newLocation.setX(Double.parseDouble(parts[0].trim()));
newLocation.setY(Double.parseDouble(parts[1].trim()));
newLocation.setZ(Double.parseDouble(parts[2].trim()));
return newLocation;
} else {
return player.getLocation().clone();
}
}
/**
* Parses the given material name
*
* @param materialName <p>The material name to parse</p>
* @return <p>The parsed material, or AIR if not valid</p>
*/
protected @NotNull Material parseMaterial(String materialName) {
Material material = Material.matchMaterial(materialName);
if (material == null) {
material = Material.AIR;
}
return material;
}
/**
* Splits the given string on comma, and returns a set of its parts
*
* @param input <p>The input string to get as a set</p>
* @return <p>The resulting string set</p>
*/
@Nullable
protected Set<String> asSet(@NotNull String input) {
if (InputValidationHelper.isEmptyValue(input)) {
return null;
} else {
return new HashSet<>(List.of(input.split(",")));
}
}
}

View File

@ -0,0 +1,103 @@
package net.knarcraft.minigames.command;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaGroup;
import net.knarcraft.minigames.arena.ArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* A command for listing the arenas in a group
*
* @param <K> <p>The type of arena handler to get arenas from</p>
* @param <L> <p>The type of arena to list</p>
* @param <M> <p>The type of arena group to list</p>
*/
public abstract class GroupListCommand<
K extends ArenaHandler<L, M>,
L extends Arena,
M extends ArenaGroup<L, M>
> implements TabExecutor {
/**
* Displays all currently existing dropper arena groups
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the groups to</p>
*/
protected void displayExistingGroups(@NotNull K arenaHandler, @NotNull CommandSender sender) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
StringBuilder builder = new StringBuilder(stringFormatter.getUnFormattedMessage(
MiniGameMessage.SUCCESS_GROUPS)).append("\n");
arenaHandler.getAllGroups().stream().sorted().forEachOrdered((group) ->
builder.append("- ").append(group.getGroupName()).append("\n"));
stringFormatter.displaySuccessMessage(sender, builder.toString());
}
/**
* Displays the ordered stages in a specified group to the specified command sender
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the stages to</p>
* @param groupName <p>The name of the group to display stages for</p>
* @return <p>True if the stages were successfully displayed</p>
*/
protected boolean displayOrderedArenaNames(@NotNull K arenaHandler, @NotNull CommandSender sender,
@NotNull String groupName) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
M arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
stringFormatter.displayErrorMessage(sender, MiniGameMessage.ERROR_GROUP_NOT_FOUND);
return false;
}
// Send a list of all stages (arenas in the group)
StringBuilder builder = new StringBuilder(stringFormatter.replacePlaceholder(
MiniGameMessage.SUCCESS_GROUP_STAGES, "{group}", groupName));
int counter = 1;
for (UUID arenaId : arenaGroup.getArenas()) {
L arena = arenaHandler.getArena(arenaId);
if (arena != null) {
builder.append("\n").append(counter++).append(". ").append(arena.getArenaName());
}
}
stringFormatter.displaySuccessMessage(sender, builder.toString());
return true;
}
/**
* Gets all available groups
*
* @return <p>All available groups</p>
*/
protected abstract Set<M> getGroups();
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> groupNames = new ArrayList<>();
for (M group : getGroups()) {
groupNames.add(group.getGroupName());
}
return filterMatchingContains(groupNames, arguments[0]);
} else {
return new ArrayList<>();
}
}
}

View File

@ -11,6 +11,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* An abstract class for an arena joining tab-completer
*/
@ -34,13 +36,13 @@ public abstract class JoinArenaTabCompleter implements TabCompleter {
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] arguments) {
if (arguments.length == 1) {
return arenaNameSupplier.get();
return filterMatchingContains(arenaNameSupplier.get(), arguments[0]);
} else if (arguments.length == 2) {
List<String> gameModes = new ArrayList<>();
for (ArenaGameMode gameMode : gameMode.getValues()) {
gameModes.add(gameMode.name().toLowerCase());
}
return gameModes;
return filterMatchingContains(gameModes, arguments[1]);
} else {
return new ArrayList<>();
}

View File

@ -2,6 +2,7 @@ package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -21,17 +22,19 @@ public class LeaveArenaCommand implements TabExecutor {
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
ArenaSession existingSession = MiniGames.getInstance().getSession(player.getUniqueId());
if (existingSession == null) {
commandSender.sendMessage("You are not in a mini-games arena!");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_NOT_IN_ARENA);
return false;
}
existingSession.triggerQuit(false);
existingSession.triggerQuit(false, true);
return true;
}

View File

@ -0,0 +1,41 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.GUIHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The command for opening up an arena's menu
*/
public class MenuCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
GUIHelper.openGUI(player);
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
return new ArrayList<>();
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -19,7 +20,8 @@ public class ReloadCommand implements TabExecutor {
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
MiniGames.getInstance().reload();
commandSender.sendMessage("Plugin reloaded!");
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(commandSender,
MiniGameMessage.SUCCESS_PLUGIN_RELOADED);
return true;
}

View File

@ -0,0 +1,91 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.reward.Reward;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.InputValidationHelper;
import net.knarcraft.minigames.util.RewardHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
/**
* The command used for setting arena rewards
*/
public class SetArenaRewardCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
if (arguments.length < 4) {
return false;
}
/*
/MiniGamesReward add dropper <name> <condition> <type> [data]
/MiniGamesReward add parkour <name> <condition> <type> [data]
/MiniGamesReward clear dropper <name> <condition>
/MiniGamesReward clear parkour <name> <condition>
*/
Arena arena = null;
if (arguments[1].equalsIgnoreCase("dropper")) {
arena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[2]);
} else if (arguments[1].equalsIgnoreCase("parkour")) {
arena = MiniGames.getInstance().getParkourArenaHandler().getArena(arguments[2]);
}
if (arena == null) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
RewardCondition condition = RewardCondition.getFromString(arguments[3]);
if (condition == null) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player,
MiniGameMessage.ERROR_REWARD_CONDITION_INVALID);
return false;
}
if (InputValidationHelper.isEmptyValue(arguments[0])) {
arena.clearRewards(condition);
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(player,
MiniGameMessage.SUCCESS_REWARDS_CLEARED);
return true;
}
if (!arguments[0].equalsIgnoreCase("add") || arguments.length < 5) {
return false;
}
String firstArgument = arguments.length > 5 ? arguments[5] : null;
String secondArgument = arguments.length > 6 ? arguments[6] : null;
Reward reward = RewardHelper.parseRewardInput(player, arguments[4], firstArgument, secondArgument,
Arrays.copyOfRange(arguments, 5, arguments.length));
if (reward != null) {
arena.addReward(condition, reward);
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(player,
MiniGameMessage.SUCCESS_REWARD_ADDED);
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,168 @@
package net.knarcraft.minigames.command;
import net.knarcraft.knarlib.util.MaterialHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.arena.reward.RewardType;
import net.knarcraft.minigames.util.InputValidationHelper;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingStartsWith;
/**
* The tab completer for the reward setting command
*/
public class SetArenaRewardTabCompleter implements TabCompleter {
private static final List<String> materials = getMaterials();
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
// The first argument is either clear or add
return TabCompletionHelper.filterMatchingStartsWith(Arrays.asList("add", "clear"), arguments[0]);
}
if (arguments.length >= 2) {
// If the first argument is invalid, stop further tab completion
if (!arguments[0].equalsIgnoreCase("add") && !InputValidationHelper.isEmptyValue(arguments[0])) {
return new ArrayList<>();
}
}
if (arguments.length == 2) {
// The second argument is the type of arena to change rewards for
return TabCompletionHelper.filterMatchingStartsWith(Arrays.asList("dropper", "parkour"), arguments[1]);
} else if (arguments.length == 3) {
// The third argument is the name of the arena to change rewards for
if (arguments[1].equalsIgnoreCase("dropper")) {
return filterMatchingContains(TabCompleteHelper.getDropperArenas(), arguments[2]);
} else if (arguments[1].equalsIgnoreCase("parkour")) {
return filterMatchingContains(TabCompleteHelper.getParkourArenas(), arguments[2]);
}
}
if (arguments.length >= 4) {
// Make sure a valid dropper or arena name has been given
Arena arena = null;
if (arguments[1].equalsIgnoreCase("dropper")) {
arena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[2]);
} else if (arguments[1].equalsIgnoreCase("parkour")) {
arena = MiniGames.getInstance().getParkourArenaHandler().getArena(arguments[2]);
}
if (arena == null) {
return new ArrayList<>();
}
}
if (arguments.length == 4) {
// The fourth argument is the condition to change the reward for
return filterMatchingContains(getRewardConditions(), arguments[3]);
}
if (arguments.length >= 5) {
// If the condition is invalid, or it's the clear action, stop tab-completion
if (RewardCondition.getFromString(arguments[3]) == null || InputValidationHelper.isEmptyValue(arguments[0])) {
return new ArrayList<>();
}
}
if (arguments.length == 5) {
// The fifth argument is the type of reward to grant
return filterMatchingContains(getRewardTypes(), arguments[4]);
}
if (arguments.length >= 6) {
// Make sure a valid reward type has been given
RewardType rewardType = RewardType.getFromString(arguments[4]);
if (rewardType == null) {
return new ArrayList<>();
}
if (arguments.length == 6) {
return switch (rewardType) {
case ITEM -> filterMatchingContains(materials, arguments[5]);
case PERMISSION -> TabCompleteHelper.tabCompletePermission(arguments[5]);
case ECONOMY -> filterMatchingStartsWith(Arrays.asList("1", "5", "10", "25", "50"), arguments[5]);
case COMMAND -> filterMatchingStartsWith(getCommands(), arguments[5]);
};
}
if (rewardType == RewardType.ITEM && arguments.length == 7) {
// If a valid item material has been given, give potential amounts
if (MaterialHelper.loadMaterialString(arguments[5], MiniGames.getInstance().getLogger()) == null) {
return new ArrayList<>();
}
return Arrays.asList("1", "5", "10", "16", "32", "48", "64");
}
}
return new ArrayList<>();
}
/**
* Gets example command rewards
*
* @return <p>Example command rewards</p>
*/
private static List<String> getCommands() {
List<String> commands = new ArrayList<>();
commands.add("f powerboost player add %player% 1");
commands.add("minecraft:xp give (player_name) 1000");
return commands;
}
/**
* Gets all materials grant-able as item rewards
*
* @return <p>All grant-able materials</p>
*/
private static List<String> getMaterials() {
List<String> materials = new ArrayList<>();
Set<Material> invalid = EnumSet.of(Material.WATER, Material.LAVA, Material.POWDER_SNOW);
for (Material material : Material.values()) {
if (material.isAir() || invalid.contains(material) || (material.isBlock() &&
(material.getHardness() == -1 || material.getHardness() == Double.MAX_VALUE))) {
continue;
}
materials.add(material.name());
}
return materials;
}
/**
* Gets a list of all reward types
*
* @return <p>All reward types</p>
*/
private List<String> getRewardTypes() {
List<String> types = new ArrayList<>();
for (RewardType rewardType : RewardType.values()) {
types.add(rewardType.name());
}
return types;
}
/**
* Gets a list of all reward conditions
*
* @return <p>All reward conditions</p>
*/
private List<String> getRewardConditions() {
List<String> conditions = new ArrayList<>();
for (RewardCondition rewardCondition : RewardCondition.values()) {
conditions.add(rewardCondition.name());
}
return conditions;
}
}

View File

@ -3,6 +3,7 @@ package net.knarcraft.minigames.command.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -19,7 +20,8 @@ public class CreateDropperArenaCommand implements CommandExecutor {
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
@ -37,16 +39,17 @@ public class CreateDropperArenaCommand implements CommandExecutor {
}
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena existingArena = arenaHandler.getArena(arenaName);
if (existingArena != null) {
commandSender.sendMessage("There already exists a dropper arena with that name!");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_ARENA_NAME_COLLISION);
return false;
}
DropperArena arena = new DropperArena(arenaName, player.getLocation(), arenaHandler);
arenaHandler.addArena(arena);
commandSender.sendMessage("The arena was successfully created!");
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(commandSender,
MiniGameMessage.SUCCESS_ARENA_CREATED);
return true;
}

View File

@ -4,20 +4,17 @@ import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.command.GroupListCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.Set;
/**
* The command for listing groups and the stages within
*/
public class DropperGroupListCommand implements TabExecutor {
public class DropperGroupListCommand extends GroupListCommand<DropperArenaHandler, DropperArena, DropperArenaGroup> {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@ -33,61 +30,9 @@ public class DropperGroupListCommand implements TabExecutor {
}
}
/**
* Displays all currently existing dropper arena groups
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the groups to</p>
*/
private void displayExistingGroups(@NotNull DropperArenaHandler arenaHandler, @NotNull CommandSender sender) {
StringBuilder builder = new StringBuilder("Dropper arena groups:").append("\n");
arenaHandler.getAllGroups().stream().sorted().forEachOrdered((group) ->
builder.append(group.getGroupName()).append("\n"));
sender.sendMessage(builder.toString());
}
/**
* Displays the ordered stages in a specified group to the specified command sender
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the stages to</p>
* @param groupName <p>The name of the group to display stages for</p>
* @return <p>True if the stages were successfully displayed</p>
*/
private boolean displayOrderedArenaNames(@NotNull DropperArenaHandler arenaHandler, @NotNull CommandSender sender,
@NotNull String groupName) {
DropperArenaGroup arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
sender.sendMessage("Unable to find the specified group!");
return false;
}
// Send a list of all stages (arenas in the group)
StringBuilder builder = new StringBuilder(groupName).append("'s stages:").append("\n");
int counter = 1;
for (UUID arenaId : arenaGroup.getArenas()) {
DropperArena arena = arenaHandler.getArena(arenaId);
if (arena != null) {
builder.append(counter++).append(". ").append(arena.getArenaName()).append("\n");
}
}
sender.sendMessage(builder.toString());
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> groupNames = new ArrayList<>();
for (DropperArenaGroup group : MiniGames.getInstance().getDropperArenaHandler().getAllGroups()) {
groupNames.add(group.getGroupName());
}
return groupNames;
} else {
return new ArrayList<>();
}
protected Set<DropperArenaGroup> getGroups() {
return MiniGames.getInstance().getDropperArenaHandler().getAllGroups();
}
}

View File

@ -4,6 +4,8 @@ import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.InputValidationHelper;
import net.knarcraft.minigames.util.StringSanitizer;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
@ -15,6 +17,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The command for setting the group of an arena
*/
@ -31,7 +35,8 @@ public class DropperGroupSetCommand implements TabExecutor {
DropperArena specifiedArena = arenaHandler.getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,
MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
@ -42,7 +47,7 @@ public class DropperGroupSetCommand implements TabExecutor {
}
DropperArenaGroup arenaGroup;
if (groupName.equalsIgnoreCase("null") || groupName.equalsIgnoreCase("none")) {
if (InputValidationHelper.isEmptyValue(groupName)) {
arenaGroup = null;
} else {
arenaGroup = arenaHandler.getGroup(groupName);
@ -53,7 +58,8 @@ public class DropperGroupSetCommand implements TabExecutor {
arenaHandler.setGroup(specifiedArena.getArenaId(), arenaGroup);
commandSender.sendMessage("The arena's group has been updated");
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(commandSender,
MiniGameMessage.SUCCESS_ARENA_GROUP_UPDATED);
return true;
}
@ -62,7 +68,7 @@ public class DropperGroupSetCommand implements TabExecutor {
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getDropperArenas();
return filterMatchingContains(TabCompleteHelper.getDropperArenas(), arguments[0]);
} else if (arguments.length == 2) {
List<String> possibleValues = new ArrayList<>();
possibleValues.add("none");
@ -70,7 +76,7 @@ public class DropperGroupSetCommand implements TabExecutor {
for (DropperArenaGroup group : MiniGames.getInstance().getDropperArenaHandler().getAllGroups()) {
possibleValues.add(group.getGroupName());
}
return possibleValues;
return filterMatchingContains(possibleValues, arguments[1]);
} else {
return new ArrayList<>();
}

View File

@ -1,9 +1,11 @@
package net.knarcraft.minigames.command.dropper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -14,6 +16,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The command for swapping the order of two arenas in a group
*/
@ -26,30 +30,31 @@ public class DropperGroupSwapCommand implements TabExecutor {
return false;
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena arena1 = arenaHandler.getArena(arguments[0]);
if (arena1 == null) {
commandSender.sendMessage("Unable to find the first specified dropper arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_1_NOT_FOUND);
return false;
}
DropperArena arena2 = arenaHandler.getArena(arguments[1]);
if (arena2 == null) {
commandSender.sendMessage("Unable to find the second specified dropper arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_2_NOT_FOUND);
return false;
}
DropperArenaGroup arena1Group = arenaHandler.getGroup(arena1.getArenaId());
DropperArenaGroup arena2Group = arenaHandler.getGroup(arena2.getArenaId());
if (arena1Group == null || !arena1Group.equals(arena2Group)) {
commandSender.sendMessage("You cannot swap arenas in different groups!");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_SWAP_DIFFERENT_GROUPS);
return false;
}
arena1Group.swapArenas(arena1Group.getArenas().indexOf(arena1.getArenaId()),
arena1Group.getArenas().indexOf(arena2.getArenaId()));
commandSender.sendMessage("The arenas have been swapped!");
stringFormatter.displaySuccessMessage(commandSender, MiniGameMessage.SUCCESS_ARENAS_SWAPPED);
return true;
}
@ -63,9 +68,9 @@ public class DropperGroupSwapCommand implements TabExecutor {
for (DropperArena dropperArena : arenaHandler.getArenasInAGroup()) {
arenaNames.add(dropperArena.getArenaName());
}
return arenaNames;
return filterMatchingContains(arenaNames, arguments[0]);
} else if (arguments.length == 2) {
return getArenaNamesInSameGroup(arguments[0]);
return filterMatchingContains(getArenaNamesInSameGroup(arguments[0]), arguments[1]);
} else {
return new ArrayList<>();
}

View File

@ -1,13 +1,14 @@
package net.knarcraft.minigames.command.dropper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaEditableProperty;
import net.knarcraft.minigames.command.EditArenaCommand;
import net.knarcraft.minigames.config.DropperConfiguration;
import org.bukkit.Location;
import org.bukkit.Material;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.InputValidationHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@ -15,9 +16,7 @@ import org.jetbrains.annotations.NotNull;
/**
* The command for editing an existing dropper arena
*/
public class EditDropperArenaCommand implements CommandExecutor {
private final DropperConfiguration configuration;
public class EditDropperArenaCommand extends EditArenaCommand {
/**
* Instantiates a new edit arena command
@ -25,14 +24,15 @@ public class EditDropperArenaCommand implements CommandExecutor {
* @param configuration <p>The configuration to use</p>
*/
public EditDropperArenaCommand(DropperConfiguration configuration) {
this.configuration = configuration;
super(configuration);
}
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
@ -42,29 +42,31 @@ public class EditDropperArenaCommand implements CommandExecutor {
DropperArena specifiedArena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
DropperArenaEditableProperty editableProperty = DropperArenaEditableProperty.getFromArgumentString(arguments[1]);
if (editableProperty == null) {
commandSender.sendMessage("Unknown property specified.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_UNKNOWN_PROPERTY);
return false;
}
String currentValueFormat = "Current value of %s is: %s";
if (arguments.length < 3) {
// Print the current value of the property
String value = editableProperty.getCurrentValueAsString(specifiedArena);
commandSender.sendMessage(String.format(currentValueFormat, editableProperty.getArgumentString(), value));
stringFormatter.displaySuccessMessage(commandSender, stringFormatter.replacePlaceholders(
MiniGameMessage.SUCCESS_CURRENT_VALUE, new String[]{"{property}", "{value}"},
new String[]{editableProperty.getArgumentString(), value}));
return true;
} else {
boolean successful = changeValue(specifiedArena, editableProperty, arguments[2], player);
if (successful) {
player.sendMessage(String.format("Property %s changed to: %s", editableProperty, arguments[2]));
stringFormatter.displaySuccessMessage(player, stringFormatter.replacePlaceholder(
MiniGameMessage.SUCCESS_PROPERTY_CHANGED, "{property}",
editableProperty.getArgumentString()));
} else {
player.sendMessage("Unable to change the property. Make sure your input is valid!");
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_PROPERTY_INPUT_INVALID);
}
return successful;
}
@ -88,80 +90,12 @@ public class EditDropperArenaCommand implements CommandExecutor {
case SPAWN_LOCATION -> arena.setSpawnLocation(parseLocation(player, value));
case NAME -> arena.setName(value);
case EXIT_LOCATION -> arena.setExitLocation(parseLocation(player, value));
case MAX_PLAYERS -> arena.setMaxPlayers(parseMaxPlayers(value));
case ALLOWED_DAMAGE_CAUSES ->
arena.setAllowedDamageCauses(InputValidationHelper.parseDamageCauses(asSet(value)));
case LOSS_TRIGGER_DAMAGE_CAUSES ->
arena.setLossTriggerDamageCauses(InputValidationHelper.parseDamageCauses(asSet(value)));
};
}
/**
* Sanitizes the player's specified vertical velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 0.5 if not parse-able</p>
*/
private double sanitizeVerticalVelocity(@NotNull String velocityString) {
// Vertical velocity should not be negative, as it would make the player go upwards. There is technically not a
// max speed limit, but setting it too high makes the arena unplayable
double velocity;
try {
velocity = Double.parseDouble(velocityString);
} catch (NumberFormatException exception) {
velocity = configuration.getVerticalVelocity();
}
// Require at least speed of 0.001, and at most 75 blocks/s
return Math.min(Math.max(velocity, 0.001), 75);
}
/**
* Sanitizes the user's specified horizontal velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 1 if not parse-able</p>
*/
private float sanitizeHorizontalVelocity(@NotNull String velocityString) {
// Horizontal velocity is valid between -1 and 1, where negative values swaps directions
float velocity;
try {
velocity = Float.parseFloat(velocityString);
} catch (NumberFormatException exception) {
velocity = configuration.getHorizontalVelocity();
}
// If outside bonds, choose the most extreme value
return Math.min(Math.max(0.1f, velocity), 1);
}
/**
* Parses the given location string
*
* @param player <p>The player changing a location</p>
* @param locationString <p>The location string to parse</p>
* @return <p>The parsed location, or the player's location if not parse-able</p>
*/
private @NotNull Location parseLocation(Player player, String locationString) {
if ((locationString.trim() + ",").matches("([0-9]+.?[0-9]*,){3}")) {
String[] parts = locationString.split(",");
Location newLocation = player.getLocation().clone();
newLocation.setX(Double.parseDouble(parts[0].trim()));
newLocation.setY(Double.parseDouble(parts[1].trim()));
newLocation.setZ(Double.parseDouble(parts[2].trim()));
return newLocation;
} else {
return player.getLocation().clone();
}
}
/**
* Parses the given material name
*
* @param materialName <p>The material name to parse</p>
* @return <p>The parsed material, or AIR if not valid</p>
*/
private @NotNull Material parseMaterial(String materialName) {
Material material = Material.matchMaterial(materialName);
if (material == null) {
material = Material.AIR;
}
return material;
}
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.minigames.command.dropper;
import net.knarcraft.minigames.arena.EditablePropertyType;
import net.knarcraft.minigames.arena.dropper.DropperArenaEditableProperty;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -10,6 +12,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The tab-completer for the edit arena command
*/
@ -17,14 +21,24 @@ public class EditDropperArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
return TabCompleteHelper.getDropperArenas();
} else if (args.length == 2) {
return TabCompleteHelper.getDropperArenaProperties();
} else if (args.length == 3) {
//TODO: Tab-complete possible values for the given property
return null;
@NotNull String label, @NotNull String[] arguments) {
if (arguments.length == 1) {
return filterMatchingContains(TabCompleteHelper.getDropperArenas(), arguments[0]);
} else if (arguments.length == 2) {
return filterMatchingContains(TabCompleteHelper.getDropperArenaProperties(), arguments[1]);
} else if (arguments.length == 3) {
DropperArenaEditableProperty property = DropperArenaEditableProperty.getFromArgumentString(arguments[1]);
if (property == null) {
return new ArrayList<>();
}
EditablePropertyType propertyType = property.getPropertyType();
if (propertyType == EditablePropertyType.MATERIAL_LIST ||
propertyType == EditablePropertyType.DAMAGE_CAUSE_LIST) {
return TabCompleteHelper.getListCompleteSuggestions(propertyType, arguments[2]);
} else {
return filterMatchingContains(TabCompleteHelper.getTabCompleteSuggestions(propertyType), arguments[2]);
}
} else {
return new ArrayList<>();
}

View File

@ -1,12 +1,16 @@
package net.knarcraft.minigames.command.dropper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.GUIHelper;
import net.knarcraft.minigames.util.GeyserHelper;
import net.knarcraft.minigames.util.PlayerTeleporter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -22,8 +26,9 @@ public class JoinDropperArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
@ -31,26 +36,39 @@ public class JoinDropperArenaCommand implements CommandExecutor {
return false;
}
if (GeyserHelper.isGeyserPlayer(player)) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_GEYSER_DROPPER);
return true;
}
// Disallow joining if the player is already in a mini-game arena
if (MiniGames.getInstance().getSession(player.getUniqueId()) != null) {
commandSender.sendMessage("You are already playing a mini-game!");
return false;
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ALREADY_PLAYING);
return true;
}
// Make sure the arena exists
DropperArena specifiedArena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
// Deny vehicles as allowing this is tricky, and will cause problems in some cases
if (player.isInsideVehicle() || !player.getPassengers().isEmpty()) {
commandSender.sendMessage("You cannot join an arena while inside a vehicle or carrying a passenger.");
return false;
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_JOIN_IN_VEHICLE_OR_PASSENGER);
return true;
}
return joinArena(specifiedArena, player, arguments);
// Deny joining full arenas
int playingNow = MiniGames.getInstance().getDropperArenaPlayerRegistry().getPlayingPlayers(specifiedArena).size();
if (specifiedArena.getMaxPlayers() > 0 && playingNow >= specifiedArena.getMaxPlayers()) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_JOIN_ARENA_FULL);
return true;
}
joinArena(specifiedArena, player, arguments);
return true;
}
/**
@ -59,13 +77,12 @@ public class JoinDropperArenaCommand implements CommandExecutor {
* @param specifiedArena <p>The arena the player wants to join</p>
* @param player <p>The player joining the arena</p>
* @param arguments <p>The arguments given</p>
* @return <p>Whether the arena was joined successfully</p>
*/
private boolean joinArena(DropperArena specifiedArena, Player player, String[] arguments) {
private void joinArena(DropperArena specifiedArena, Player player, String[] arguments) {
// Find the specified game-mode
DropperArenaGameMode gameMode;
if (arguments.length > 1) {
gameMode = DropperArenaGameMode.matchGamemode(arguments[1]);
gameMode = DropperArenaGameMode.matchGameMode(arguments[1]);
} else {
gameMode = DropperArenaGameMode.DEFAULT;
}
@ -73,33 +90,38 @@ public class JoinDropperArenaCommand implements CommandExecutor {
// Make sure the player has beaten the necessary levels
DropperArenaGroup arenaGroup = MiniGames.getInstance().getDropperArenaHandler().getGroup(specifiedArena.getArenaId());
if (arenaGroup != null && !doGroupChecks(specifiedArena, arenaGroup, gameMode, player)) {
return false;
return;
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
// Make sure the player has beaten the arena once in normal mode before playing another mode
if (MiniGames.getInstance().getDropperConfiguration().mustDoNormalModeFirst() &&
gameMode != DropperArenaGameMode.DEFAULT &&
specifiedArena.getData().hasNotCompleted(DropperArenaGameMode.DEFAULT, player)) {
player.sendMessage("You must complete this arena in normal mode first!");
return false;
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_NORMAL_MODE_REQUIRED);
return;
}
// Register the player's session
DropperArenaSession newSession = new DropperArenaSession(specifiedArena, player, gameMode);
DropperArenaPlayerRegistry playerRegistry = MiniGames.getInstance().getDropperArenaPlayerRegistry();
ArenaPlayerRegistry<DropperArena> playerRegistry = MiniGames.getInstance().getDropperArenaPlayerRegistry();
playerRegistry.registerPlayer(player.getUniqueId(), newSession);
// Update visibility and hit-box for the player
MiniGames.getInstance().getPlayerVisibilityManager().updateHiddenPlayers(playerRegistry, player);
// Try to teleport the player to the arena
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false, false);
if (!teleported) {
player.sendMessage("Unable to teleport you to the dropper arena. Make sure you're not in a vehicle," +
"and not carrying a passenger!");
newSession.triggerQuit(false);
return false;
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_ARENA_TELEPORT_FAILED);
newSession.triggerQuit(false, true);
} else {
// Make sure to update the state again in the air to remove a potential swimming state
// Update the player's state to follow the arena's rules
newSession.getEntryState().setArenaState();
return true;
player.getInventory().addItem(GUIHelper.getGUIOpenItem(player));
stringFormatter.displaySuccessMessage(player, MiniGameMessage.SUCCESS_ARENA_JOINED);
}
}
@ -114,18 +136,19 @@ public class JoinDropperArenaCommand implements CommandExecutor {
*/
private boolean doGroupChecks(@NotNull DropperArena dropperArena, @NotNull DropperArenaGroup arenaGroup,
@NotNull DropperArenaGameMode arenaGameMode, @NotNull Player player) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
DropperConfiguration configuration = MiniGames.getInstance().getDropperConfiguration();
// Require that players beat all arenas in the group in the normal game-mode before trying challenge modes
if (configuration.mustDoNormalModeFirst() && arenaGameMode != DropperArenaGameMode.DEFAULT &&
!arenaGroup.hasBeatenAll(DropperArenaGameMode.DEFAULT, player)) {
player.sendMessage("You have not yet beaten all arenas in this group!");
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_GROUP_NORMAL_MODE_REQUIRED);
return false;
}
// Require that the player has beaten the previous arena on the same game-mode before trying this one
if (configuration.mustDoGroupedInSequence() &&
arenaGroup.cannotPlay(arenaGameMode, player, dropperArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_PREVIOUS_ARENA_REQUIRED);
return false;
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.minigames.command.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -18,10 +20,12 @@ public class ListDropperArenaCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
sender.sendMessage("Dropper arenas:");
StringBuilder builder = new StringBuilder(MiniGames.getInstance().getTranslator().getTranslatedMessage(
MiniGameMessage.SUCCESS_DROPPER_ARENAS_LIST));
for (String arenaName : TabCompleteHelper.getDropperArenas()) {
sender.sendMessage(arenaName);
builder.append("\n").append(arenaName);
}
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(sender, builder.toString());
return true;
}

View File

@ -1,7 +1,9 @@
package net.knarcraft.minigames.command.dropper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -20,16 +22,18 @@ public class RemoveDropperArenaCommand implements CommandExecutor {
return false;
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
// Get the specified arena
DropperArena targetArena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[0]);
if (targetArena == null) {
commandSender.sendMessage("Unable to find the specified arena");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
// Remove the arena
MiniGames.getInstance().getDropperArenaHandler().removeArena(targetArena);
commandSender.sendMessage("The specified arena has been successfully removed");
stringFormatter.displaySuccessMessage(commandSender, MiniGameMessage.SUCCESS_ARENA_REMOVED);
return true;
}

View File

@ -10,6 +10,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The tab-completer for the remove arena command
*/
@ -20,7 +22,7 @@ public class RemoveDropperArenaTabCompleter implements TabCompleter {
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getDropperArenas();
return filterMatchingContains(TabCompleteHelper.getDropperArenas(), arguments[0]);
} else {
return new ArrayList<>();
}

View File

@ -1,8 +1,10 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -18,8 +20,9 @@ public class CreateParkourArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
@ -37,16 +40,15 @@ public class CreateParkourArenaCommand implements CommandExecutor {
}
ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler();
ParkourArena existingArena = arenaHandler.getArena(arenaName);
if (existingArena != null) {
commandSender.sendMessage("There already exists a parkour arena with that name!");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NAME_COLLISION);
return false;
}
ParkourArena arena = new ParkourArena(arenaName, player.getLocation(), arenaHandler);
arenaHandler.addArena(arena);
commandSender.sendMessage("The arena was successfully created!");
stringFormatter.displaySuccessMessage(commandSender, MiniGameMessage.SUCCESS_ARENA_CREATED);
return true;
}

View File

@ -1,35 +1,35 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaEditableProperty;
import org.bukkit.Location;
import org.bukkit.Material;
import net.knarcraft.minigames.command.EditArenaCommand;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.InputValidationHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
/**
* The command for editing an existing dropper arena
*/
public class EditParkourArenaCommand implements CommandExecutor {
public class EditParkourArenaCommand extends EditArenaCommand {
/**
* Instantiates a new edit arena command
*/
public EditParkourArenaCommand() {
super(null);
}
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
@ -39,29 +39,36 @@ public class EditParkourArenaCommand implements CommandExecutor {
ParkourArena specifiedArena = MiniGames.getInstance().getParkourArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
ParkourArenaEditableProperty editableProperty = ParkourArenaEditableProperty.getFromArgumentString(arguments[1]);
if (editableProperty == null) {
commandSender.sendMessage("Unknown property specified.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_UNKNOWN_PROPERTY);
return false;
}
String currentValueFormat = "Current value of %s is: %s";
if (arguments.length < 3) {
// Print the current value of the property
String value = editableProperty.getCurrentValueAsString(specifiedArena);
commandSender.sendMessage(String.format(currentValueFormat, editableProperty.getArgumentString(), value));
stringFormatter.displaySuccessMessage(commandSender, stringFormatter.replacePlaceholders(
MiniGameMessage.SUCCESS_CURRENT_VALUE, new String[]{"{property}", "{value}"},
new String[]{editableProperty.getArgumentString(), value}));
return true;
} else {
boolean successful = changeValue(specifiedArena, editableProperty, arguments[2], player);
boolean successful;
try {
successful = changeValue(specifiedArena, editableProperty, arguments[2], player);
} catch (NumberFormatException exception) {
successful = false;
}
if (successful) {
player.sendMessage(String.format("Property %s changed to: %s", editableProperty, arguments[2]));
stringFormatter.displaySuccessMessage(player, stringFormatter.replacePlaceholder(
MiniGameMessage.SUCCESS_PROPERTY_CHANGED, "{property}",
editableProperty.getArgumentString()));
} else {
player.sendMessage("Unable to change the property. Make sure your input is valid!");
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_PROPERTY_INPUT_INVALID);
}
return successful;
}
@ -75,9 +82,10 @@ public class EditParkourArenaCommand implements CommandExecutor {
* @param value <p>The new value of the property</p>
* @param player <p>The player trying to change the value</p>
* @return <p>True if the value was successfully changed</p>
* @throws NumberFormatException <p>If unable to parse a given numeric value</p>
*/
private boolean changeValue(@NotNull ParkourArena arena, @NotNull ParkourArenaEditableProperty property,
@NotNull String value, @NotNull Player player) {
@NotNull String value, @NotNull Player player) throws NumberFormatException {
return switch (property) {
case WIN_BLOCK_TYPE -> arena.setWinBlockType(parseMaterial(value));
case SPAWN_LOCATION -> arena.setSpawnLocation(parseLocation(player, value));
@ -86,42 +94,14 @@ public class EditParkourArenaCommand implements CommandExecutor {
case WIN_LOCATION -> arena.setWinLocation(parseLocation(player, value));
case CHECKPOINT_ADD -> arena.addCheckpoint(parseLocation(player, value));
case CHECKPOINT_CLEAR -> arena.clearCheckpoints();
case KILL_PLANE_BLOCKS -> arena.setKillPlaneBlocks(new HashSet<>(List.of(value.split(","))));
case KILL_PLANE_BLOCKS -> arena.setKillPlaneBlocks(asSet(value));
case OBSTACLE_BLOCKS -> arena.setObstacleBlocks(asSet(value));
case MAX_PLAYERS -> arena.setMaxPlayers(parseMaxPlayers(value));
case ALLOWED_DAMAGE_CAUSES ->
arena.setAllowedDamageCauses(InputValidationHelper.parseDamageCauses(asSet(value)));
case LOSS_TRIGGER_DAMAGE_CAUSES ->
arena.setLossTriggerDamageCauses(InputValidationHelper.parseDamageCauses(asSet(value)));
};
}
/**
* Parses the given location string
*
* @param player <p>The player changing a location</p>
* @param locationString <p>The location string to parse</p>
* @return <p>The parsed location, or the player's location if not parse-able</p>
*/
private @NotNull Location parseLocation(Player player, String locationString) {
if ((locationString.trim() + ",").matches("([0-9]+.?[0-9]*,){3}")) {
String[] parts = locationString.split(",");
Location newLocation = player.getLocation().clone();
newLocation.setX(Double.parseDouble(parts[0].trim()));
newLocation.setY(Double.parseDouble(parts[1].trim()));
newLocation.setZ(Double.parseDouble(parts[2].trim()));
return newLocation;
} else {
return player.getLocation().clone();
}
}
/**
* Parses the given material name
*
* @param materialName <p>The material name to parse</p>
* @return <p>The parsed material, or AIR if not valid</p>
*/
private @NotNull Material parseMaterial(String materialName) {
Material material = Material.matchMaterial(materialName);
if (material == null) {
material = Material.AIR;
}
return material;
}
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.minigames.arena.EditablePropertyType;
import net.knarcraft.minigames.arena.parkour.ParkourArenaEditableProperty;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -10,6 +12,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The tab-completer for the edit arena command
*/
@ -17,14 +21,24 @@ public class EditParkourArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
return TabCompleteHelper.getParkourArenas();
} else if (args.length == 2) {
return TabCompleteHelper.getParkourArenaProperties();
} else if (args.length == 3) {
//TODO: Tab-complete possible values for the given property
return null;
@NotNull String label, @NotNull String[] arguments) {
if (arguments.length == 1) {
return filterMatchingContains(TabCompleteHelper.getParkourArenas(), arguments[0]);
} else if (arguments.length == 2) {
return filterMatchingContains(TabCompleteHelper.getParkourArenaProperties(), arguments[1]);
} else if (arguments.length == 3) {
ParkourArenaEditableProperty property = ParkourArenaEditableProperty.getFromArgumentString(arguments[1]);
if (property == null) {
return new ArrayList<>();
}
EditablePropertyType propertyType = property.getPropertyType();
if (propertyType == EditablePropertyType.MATERIAL_LIST ||
propertyType == EditablePropertyType.DAMAGE_CAUSE_LIST) {
return TabCompleteHelper.getListCompleteSuggestions(propertyType, arguments[2]);
} else {
return filterMatchingContains(TabCompleteHelper.getTabCompleteSuggestions(propertyType), arguments[2]);
}
} else {
return new ArrayList<>();
}

View File

@ -1,12 +1,15 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGameMode;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGroup;
import net.knarcraft.minigames.arena.parkour.ParkourArenaPlayerRegistry;
import net.knarcraft.minigames.arena.parkour.ParkourArenaSession;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.util.GUIHelper;
import net.knarcraft.minigames.util.PlayerTeleporter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -22,8 +25,9 @@ public class JoinParkourArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_PLAYER_ONLY);
return false;
}
@ -33,24 +37,32 @@ public class JoinParkourArenaCommand implements CommandExecutor {
// Disallow joining if the player is already in a mini-game arena
if (MiniGames.getInstance().getSession(player.getUniqueId()) != null) {
commandSender.sendMessage("You are already playing a mini-game!");
return false;
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ALREADY_PLAYING);
return true;
}
// Make sure the arena exists
ParkourArena specifiedArena = MiniGames.getInstance().getParkourArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified parkour arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
// Deny vehicles as allowing this is tricky, and will cause problems in some cases
if (player.isInsideVehicle() || !player.getPassengers().isEmpty()) {
commandSender.sendMessage("You cannot join an arena while inside a vehicle or carrying a passenger.");
return false;
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_JOIN_IN_VEHICLE_OR_PASSENGER);
return true;
}
return joinArena(specifiedArena, player, arguments);
// Deny joining full arenas
int playingNow = MiniGames.getInstance().getParkourArenaPlayerRegistry().getPlayingPlayers(specifiedArena).size();
if (specifiedArena.getMaxPlayers() > 0 && playingNow >= specifiedArena.getMaxPlayers()) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_JOIN_ARENA_FULL);
return true;
}
joinArena(specifiedArena, player, arguments);
return true;
}
/**
@ -59,39 +71,50 @@ public class JoinParkourArenaCommand implements CommandExecutor {
* @param specifiedArena <p>The arena the player wants to join</p>
* @param player <p>The player joining the arena</p>
* @param arguments <p>The arguments given</p>
* @return <p>Whether the arena was joined successfully</p>
*/
private boolean joinArena(ParkourArena specifiedArena, Player player, String[] arguments) {
private void joinArena(ParkourArena specifiedArena, Player player, String[] arguments) {
// Find the specified game-mode
ParkourArenaGameMode gameMode;
if (arguments.length > 1) {
gameMode = ParkourArenaGameMode.matchGamemode(arguments[1]);
gameMode = ParkourArenaGameMode.matchGameMode(arguments[1]);
} else {
gameMode = ParkourArenaGameMode.DEFAULT;
}
// Don't allow joining the hardcore game-mode if there are no checkpoints to skip
if (specifiedArena.hasNoCheckpoints() && gameMode == ParkourArenaGameMode.HARDCORE) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player,
MiniGameMessage.ERROR_HARDCORE_NO_CHECKPOINTS);
return;
}
// Make sure the player has beaten the necessary levels
ParkourArenaGroup arenaGroup = MiniGames.getInstance().getParkourArenaHandler().getGroup(specifiedArena.getArenaId());
if (arenaGroup != null && !doGroupChecks(specifiedArena, arenaGroup, gameMode, player)) {
return false;
return;
}
// Register the player's session
ParkourArenaSession newSession = new ParkourArenaSession(specifiedArena, player, gameMode);
ParkourArenaPlayerRegistry playerRegistry = MiniGames.getInstance().getParkourArenaPlayerRegistry();
ArenaPlayerRegistry<ParkourArena> playerRegistry = MiniGames.getInstance().getParkourArenaPlayerRegistry();
playerRegistry.registerPlayer(player.getUniqueId(), newSession);
// Update visibility and hit-box for the player
MiniGames.getInstance().getPlayerVisibilityManager().updateHiddenPlayers(playerRegistry, player);
// Try to teleport the player to the arena
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false, false);
if (!teleported) {
player.sendMessage("Unable to teleport you to the parkour arena. Make sure you're not in a vehicle," +
"and not carrying a passenger!");
newSession.triggerQuit(false);
return false;
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player,
MiniGameMessage.ERROR_ARENA_TELEPORT_FAILED);
newSession.triggerQuit(false, true);
} else {
// Make sure to update the state again in the air to remove a potential swimming state
// Update the player's state to follow the arena's rules
newSession.getEntryState().setArenaState();
return true;
player.getInventory().addItem(GUIHelper.getGUIOpenItem(player));
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(player,
MiniGameMessage.SUCCESS_ARENA_JOINED);
}
}
@ -111,7 +134,8 @@ public class JoinParkourArenaCommand implements CommandExecutor {
// Require that the player has beaten the previous arena on the same game-mode before trying this one
if (configuration.mustDoGroupedInSequence() &&
arenaGroup.cannotPlay(arenaGameMode, player, parkourArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player,
MiniGameMessage.ERROR_PREVIOUS_ARENA_REQUIRED);
return false;
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -18,10 +20,12 @@ public class ListParkourArenaCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
sender.sendMessage("Parkour arenas:");
StringBuilder builder = new StringBuilder(MiniGames.getInstance().getTranslator().getTranslatedMessage(
MiniGameMessage.SUCCESS_PARKOUR_ARENAS_LIST));
for (String arenaName : TabCompleteHelper.getParkourArenas()) {
sender.sendMessage(arenaName);
builder.append("\n").append(arenaName);
}
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(sender, builder.toString());
return true;
}

View File

@ -4,21 +4,17 @@ import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGroup;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.command.GroupListCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
* The command for listing groups and the stages within
*/
public class ParkourGroupListCommand implements TabExecutor {
public class ParkourGroupListCommand extends GroupListCommand<ParkourArenaHandler, ParkourArena, ParkourArenaGroup> {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@ -34,62 +30,9 @@ public class ParkourGroupListCommand implements TabExecutor {
}
}
/**
* Displays all currently existing parkour arena groups
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the groups to</p>
*/
private void displayExistingGroups(@NotNull ParkourArenaHandler arenaHandler, @NotNull CommandSender sender) {
StringBuilder builder = new StringBuilder("Parkour arena groups:").append("\n");
arenaHandler.getAllGroups().stream().sorted().forEachOrdered((group) ->
builder.append(group.getGroupName()).append("\n"));
sender.sendMessage(builder.toString());
}
/**
* Displays the ordered stages in a specified group to the specified command sender
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the stages to</p>
* @param groupName <p>The name of the group to display stages for</p>
* @return <p>True if the stages were successfully displayed</p>
*/
private boolean displayOrderedArenaNames(@NotNull ParkourArenaHandler arenaHandler, @NotNull CommandSender sender,
@NotNull String groupName) {
ParkourArenaGroup arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
sender.sendMessage("Unable to find the specified group!");
return false;
}
// Send a list of all stages (arenas in the group)
StringBuilder builder = new StringBuilder(groupName).append("'s stages:").append("\n");
int counter = 1;
for (UUID arenaId : arenaGroup.getArenas()) {
ParkourArena arena = arenaHandler.getArena(arenaId);
if (arena != null) {
builder.append(counter++).append(". ").append(arena.getArenaName()).append("\n");
}
}
sender.sendMessage(builder.toString());
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> groupNames = new ArrayList<>();
Set<ParkourArenaGroup> arenaGroups = MiniGames.getInstance().getParkourArenaHandler().getAllGroups();
for (ParkourArenaGroup group : arenaGroups) {
groupNames.add(group.getGroupName());
}
return groupNames;
} else {
return new ArrayList<>();
}
protected Set<ParkourArenaGroup> getGroups() {
return MiniGames.getInstance().getParkourArenaHandler().getAllGroups();
}
}

View File

@ -1,9 +1,12 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGroup;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.util.InputValidationHelper;
import net.knarcraft.minigames.util.StringSanitizer;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
@ -15,6 +18,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The command for setting the group of an arena
*/
@ -27,11 +32,12 @@ public class ParkourGroupSetCommand implements TabExecutor {
return false;
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler();
ParkourArena specifiedArena = arenaHandler.getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified parkour arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
@ -42,7 +48,7 @@ public class ParkourGroupSetCommand implements TabExecutor {
}
ParkourArenaGroup arenaGroup;
if (groupName.equalsIgnoreCase("null") || groupName.equalsIgnoreCase("none")) {
if (InputValidationHelper.isEmptyValue(groupName)) {
arenaGroup = null;
} else {
arenaGroup = arenaHandler.getGroup(groupName);
@ -53,7 +59,7 @@ public class ParkourGroupSetCommand implements TabExecutor {
arenaHandler.setGroup(specifiedArena.getArenaId(), arenaGroup);
commandSender.sendMessage("The arena's group has been updated");
stringFormatter.displaySuccessMessage(commandSender, MiniGameMessage.SUCCESS_ARENA_GROUP_UPDATED);
return true;
}
@ -62,7 +68,7 @@ public class ParkourGroupSetCommand implements TabExecutor {
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getParkourArenas();
return filterMatchingContains(TabCompleteHelper.getParkourArenas(), arguments[0]);
} else if (arguments.length == 2) {
List<String> possibleValues = new ArrayList<>();
possibleValues.add("none");
@ -70,7 +76,7 @@ public class ParkourGroupSetCommand implements TabExecutor {
for (ParkourArenaGroup group : MiniGames.getInstance().getParkourArenaHandler().getAllGroups()) {
possibleValues.add(group.getGroupName());
}
return possibleValues;
return filterMatchingContains(possibleValues, arguments[1]);
} else {
return new ArrayList<>();
}

View File

@ -1,9 +1,11 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGroup;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -14,6 +16,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The command for swapping the order of two arenas in a group
*/
@ -27,29 +31,30 @@ public class ParkourGroupSwapCommand implements TabExecutor {
}
ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler();
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
ParkourArena arena1 = arenaHandler.getArena(arguments[0]);
if (arena1 == null) {
commandSender.sendMessage("Unable to find the first specified parkour arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_1_NOT_FOUND);
return false;
}
ParkourArena arena2 = arenaHandler.getArena(arguments[1]);
if (arena2 == null) {
commandSender.sendMessage("Unable to find the second specified parkour arena.");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_2_NOT_FOUND);
return false;
}
ParkourArenaGroup arena1Group = arenaHandler.getGroup(arena1.getArenaId());
ParkourArenaGroup arena2Group = arenaHandler.getGroup(arena2.getArenaId());
if (arena1Group == null || !arena1Group.equals(arena2Group)) {
commandSender.sendMessage("You cannot swap arenas in different groups!");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_SWAP_DIFFERENT_GROUPS);
return false;
}
arena1Group.swapArenas(arena1Group.getArenas().indexOf(arena1.getArenaId()),
arena1Group.getArenas().indexOf(arena2.getArenaId()));
commandSender.sendMessage("The arenas have been swapped!");
stringFormatter.displaySuccessMessage(commandSender, MiniGameMessage.SUCCESS_ARENAS_SWAPPED);
return true;
}
@ -63,9 +68,9 @@ public class ParkourGroupSwapCommand implements TabExecutor {
for (ParkourArena parkourArena : arenaHandler.getArenasInAGroup()) {
arenaNames.add(parkourArena.getArenaName());
}
return arenaNames;
return filterMatchingContains(arenaNames, arguments[0]);
} else if (arguments.length == 2) {
return getArenaNamesInSameGroup(arguments[0]);
return filterMatchingContains(getArenaNamesInSameGroup(arguments[0]), arguments[1]);
} else {
return new ArrayList<>();
}

View File

@ -1,7 +1,9 @@
package net.knarcraft.minigames.command.parkour;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -20,16 +22,18 @@ public class RemoveParkourArenaCommand implements CommandExecutor {
return false;
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
// Get the specified arena
ParkourArena targetArena = MiniGames.getInstance().getParkourArenaHandler().getArena(arguments[0]);
if (targetArena == null) {
commandSender.sendMessage("Unable to find the specified arena");
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NOT_FOUND);
return false;
}
// Remove the arena
MiniGames.getInstance().getParkourArenaHandler().removeArena(targetArena);
commandSender.sendMessage("The specified arena has been successfully removed");
stringFormatter.displaySuccessMessage(commandSender, MiniGameMessage.SUCCESS_ARENA_REMOVED);
return true;
}

View File

@ -10,6 +10,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.knarlib.util.TabCompletionHelper.filterMatchingContains;
/**
* The tab-completer for the remove arena command
*/
@ -20,7 +22,7 @@ public class RemoveParkourArenaTabCompleter implements TabCompleter {
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getParkourArenas();
return filterMatchingContains(TabCompleteHelper.getParkourArenas(), arguments[0]);
} else {
return new ArrayList<>();
}

View File

@ -19,8 +19,6 @@ public class DropperConfiguration extends MiniGameConfiguration {
private boolean mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce;
private boolean mustDoNormalModeFirst;
private boolean makePlayersInvisible;
private boolean disableHitCollision;
private boolean blockSneaking;
private boolean blockSprinting;
private Set<Material> blockWhitelist;
@ -97,24 +95,6 @@ public class DropperConfiguration extends MiniGameConfiguration {
return this.ignoreRecordsUntilGroupBeatenOnce;
}
/**
* Gets whether players should be made invisible while in an arena
*
* @return <p>Whether players should be made invisible</p>
*/
public boolean makePlayersInvisible() {
return this.makePlayersInvisible;
}
/**
* Gets whether entity hit-collision of players in an arena should be disabled
*
* @return <p>Whether to disable hit collision</p>
*/
public boolean disableHitCollision() {
return this.disableHitCollision;
}
/**
* Gets whether players trying to sneak while in a dropper arena to increase their downwards speed should be blocked
*
@ -141,8 +121,6 @@ public class DropperConfiguration extends MiniGameConfiguration {
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.mustDoNormalModeFirst = configuration.getBoolean(rootNode + "mustDoNormalModeFirst", true);
this.makePlayersInvisible = configuration.getBoolean(rootNode + "makePlayersInvisible", false);
this.disableHitCollision = configuration.getBoolean(rootNode + "disableHitCollision", true);
this.blockSprinting = configuration.getBoolean(rootNode + "blockSprinting", true);
this.blockSneaking = configuration.getBoolean(rootNode + "blockSneaking", true);
this.blockWhitelist = loadMaterialList(rootNode + "blockWhitelist");
@ -176,8 +154,6 @@ public class DropperConfiguration extends MiniGameConfiguration {
"\n" + "Must do groups in sequence: " + mustDoGroupedInSequence +
"\n" + "Ignore records until group beaten once: " + ignoreRecordsUntilGroupBeatenOnce +
"\n" + "Must do normal mode first: " + mustDoNormalModeFirst +
"\n" + "Make players invisible: " + makePlayersInvisible +
"\n" + "Disable hit collision: " + disableHitCollision +
"\n" + "Block whitelist: ");
for (Material material : blockWhitelist) {
builder.append("\n - ").append(material.name());

View File

@ -1,6 +1,7 @@
package net.knarcraft.minigames.config;
import net.knarcraft.minigames.util.MaterialHelper;
import net.knarcraft.knarlib.util.MaterialHelper;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
@ -46,7 +47,7 @@ public abstract class MiniGameConfiguration {
*/
public @NotNull Set<Material> loadMaterialList(@NotNull String path) {
List<?> blockWhitelist = configuration.getList(path, new ArrayList<>());
return MaterialHelper.loadMaterialList(blockWhitelist);
return MaterialHelper.loadMaterialList(blockWhitelist, "+", MiniGames.getInstance().getLogger());
}
}

View File

@ -0,0 +1,318 @@
package net.knarcraft.minigames.config;
import net.knarcraft.knarlib.formatting.TranslatableMessage;
import org.jetbrains.annotations.NotNull;
/**
* A message which ca be displayed to the user
*/
public enum MiniGameMessage implements TranslatableMessage {
/* ************** *
* Error messages *
* ************** */
/**
* The message displayed if saving arena groups fails
*/
ERROR_CANNOT_SAVE_ARENA_GROUPS,
/**
* The message displayed if an un-parse-able message is given by a user
*/
ERROR_MATERIAL_NOT_PARSE_ABLE,
/**
* The message displayed if a player tries to be teleported to/from an arena with a passenger
*/
ERROR_TELEPORT_WITH_PASSENGER,
/**
* The message displayed if a player tries to be teleported to/from an arena with a vehicle
*/
ERROR_TELEPORT_IN_VEHICLE,
/**
* The message displayed if an arena cannot be loaded
*/
ERROR_ARENA_NOT_LOADED,
/**
* The message displayed if an arena's data cannot be loaded
*/
ERROR_ARENA_DATA_NOT_LOADED,
/**
* The message displayed if the user specifies an unrecognized arena
*/
ERROR_ARENA_NOT_FOUND,
/**
* The message displayed if the user specifies an unrecognized group
*/
ERROR_GROUP_NOT_FOUND,
/**
* The message displayed if the console tries to execute a player-only command
*/
ERROR_PLAYER_ONLY,
/**
* The message displayed if the name of an arena is duplicated
*/
ERROR_ARENA_NAME_COLLISION,
/**
* The message displayed if the player is required to win on the default difficulty first
*/
ERROR_NORMAL_MODE_REQUIRED,
/**
* The message displayed if the player is required to win on the default difficulty for all arenas in the group first
*/
ERROR_GROUP_NORMAL_MODE_REQUIRED,
/**
* The message displayed if the player is required to beat the previous arena in the group
*/
ERROR_PREVIOUS_ARENA_REQUIRED,
/**
* The message displayed if player teleportation failed for some reason
*/
ERROR_ARENA_TELEPORT_FAILED,
/**
* The message displayed if the player tries to quit the arena while not in an arena
*/
ERROR_NOT_IN_ARENA,
/**
* The message displayed if the player tries to join an arena while already playing
*
* <p>This should in theory be impossible, as players cannot use any commands except /miniGamesLeave while playing
* in an arena.</p>
*/
ERROR_ALREADY_PLAYING,
/**
* The message displayed if a player tries to join an arena with a passenger or riding a vehicle
*/
ERROR_JOIN_IN_VEHICLE_OR_PASSENGER,
/**
* The message displayed if the player tries to change an unrecognized arena property
*/
ERROR_UNKNOWN_PROPERTY,
/**
* The message displayed if the given input to /dEdit or /pEdit's value is invalid
*/
ERROR_PROPERTY_INPUT_INVALID,
/**
* The message displayed if the first arena specified in /dgSwap or /pgSwap is invalid
*/
ERROR_ARENA_1_NOT_FOUND,
/**
* The message displayed if the second arena specified in /dgSwap or /pgSwap is invalid
*/
ERROR_ARENA_2_NOT_FOUND,
/**
* The message displayed if the two groups specified for /dgSwap or /pgSwap are in different arenas
*/
ERROR_SWAP_DIFFERENT_GROUPS,
/**
* The message displayed if a player tries to use any command other than /mLeave while in an arena
*/
ERROR_ILLEGAL_COMMAND,
/**
* The message displayed if the player is trying to join a parkour arena on hardcore which has no checkpoints
*/
ERROR_HARDCORE_NO_CHECKPOINTS,
/**
* The message displayed if a user specifies an invalid material
*/
ERROR_INVALID_MATERIAL,
/**
* The message displayed if a user specifies an invalid world
*/
ERROR_INVALID_WORLD,
/**
* The message displayed if a user specifies an invalid number
*/
ERROR_INVALID_NUMBER,
/**
* The message displayed if a user specifies an invalid command (for a command reward)
*/
ERROR_INVALID_COMMAND_STRING,
/**
* The message displayed if a user specified an invalid reward type
*/
ERROR_REWARD_TYPE_INVALID,
/**
* The message displayed if a user specified an invalid reward condition
*/
ERROR_REWARD_CONDITION_INVALID,
/**
* The message displayed when a geyser player tries to join a dropper arena
*/
ERROR_GEYSER_DROPPER,
/**
* The message displayed when a player attempts to join a full arena
*/
ERROR_JOIN_ARENA_FULL,
/* **************** *
* Success messages *
* **************** */
/**
* The message displayed if an arena's group has been changed
*/
SUCCESS_ARENA_GROUP_UPDATED,
/**
* The message displayed if the MiniGames plugin is reloaded
*/
SUCCESS_PLUGIN_RELOADED,
/**
* The message displayed if a new arena has been created
*/
SUCCESS_ARENA_CREATED,
/**
* The message displayed if a player clears/wins an arena for the first time
*/
SUCCESS_ARENA_FIRST_CLEAR,
/**
* The message displayed when a player wins an arena
*/
SUCCESS_ARENA_WIN,
/**
* The message displayed when a player quits an arena
*/
SUCCESS_ARENA_QUIT,
/**
* The message used to display the current value of an arena property
*/
SUCCESS_CURRENT_VALUE,
/**
* The message used to announce that an arena property has been changed
*/
SUCCESS_PROPERTY_CHANGED,
/**
* The message displayed when two arenas' order in a group have been swapped
*/
SUCCESS_ARENAS_SWAPPED,
/**
* The message displayed when an arena has been removed
*/
SUCCESS_ARENA_REMOVED,
/**
* The header displayed before listing all dropper arenas
*/
SUCCESS_DROPPER_ARENAS_LIST,
/**
* The header displayed before listing all parkour arenas
*/
SUCCESS_PARKOUR_ARENAS_LIST,
/**
* The message displayed when a player reaches a new checkpoint in a parkour arena
*/
SUCCESS_CHECKPOINT_REACHED,
/**
* The header displayed before listing all arenas (stages) in a group
*/
SUCCESS_GROUP_STAGES,
/**
* The message displayed when a new record has been achieved
*/
SUCCESS_RECORD_ACHIEVED,
/**
* The partial message used to describe that the player achieved a world record
*/
RECORD_ACHIEVED_GLOBAL,
/**
* The partial message used to describe that the player achieved a personal best record
*/
RECORD_ACHIEVED_PERSONAL,
/**
* The message displayed when a player joins an arena
*/
SUCCESS_ARENA_JOINED,
/**
* The message displayed when a player is rewarded with an item
*/
SUCCESS_ITEM_REWARDED,
/**
* The message displayed when a player is rewarded with a permission
*/
SUCCESS_PERMISSION_REWARDED,
/**
* The message displayed when a player is rewarded with a permission, for a specific world
*/
SUCCESS_PERMISSION_REWARDED_WORLD,
/**
* The message displayed when a player is rewarded by a command being run
*/
SUCCESS_COMMAND_REWARDED,
/**
* The message displayed when a player is rewarded with an amount of currency
*/
SUCCESS_ECONOMY_REWARDED,
/**
* The message displayed when an arena reward has been successfully added
*/
SUCCESS_REWARD_ADDED,
/**
* The message displayed when arena rewards have been cleared
*/
SUCCESS_REWARDS_CLEARED,
/**
* The message displayed when listing arena groups
*/
SUCCESS_GROUPS,
;
@Override
public @NotNull TranslatableMessage[] getAllMessages() {
return MiniGameMessage.values();
}
}

View File

@ -16,8 +16,8 @@ public class ParkourConfiguration extends MiniGameConfiguration {
private boolean enforceCheckpointOrder;
private boolean mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce;
private boolean makePlayersInvisible;
private Set<Material> killPlaneBlocks;
private Set<Material> obstacleBlocks;
/**
* Instantiates a new dropper configuration
@ -55,15 +55,6 @@ public class ParkourConfiguration extends MiniGameConfiguration {
return this.ignoreRecordsUntilGroupBeatenOnce;
}
/**
* Gets whether players should be made invisible while in an arena
*
* @return <p>Whether players should be made invisible</p>
*/
public boolean makePlayersInvisible() {
return this.makePlayersInvisible;
}
/**
* Gets all types of blocks constituting parkour arenas' kill planes
*
@ -73,13 +64,22 @@ public class ParkourConfiguration extends MiniGameConfiguration {
return new HashSet<>(this.killPlaneBlocks);
}
/**
* Gets all types of blocks constituting parkour arena's obstacle blocks
*
* @return <p>The types of blocks constituting parkour arena's obstacle blocks</p>
*/
public Set<Material> getObstacleBlocks() {
return new HashSet<>(this.obstacleBlocks);
}
@Override
protected void load() {
this.enforceCheckpointOrder = configuration.getBoolean(rootNode + "enforceCheckpointOrder", false);
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.makePlayersInvisible = configuration.getBoolean(rootNode + "makePlayersInvisible", false);
this.killPlaneBlocks = loadMaterialList(rootNode + "killPlaneBlocks");
this.obstacleBlocks = loadMaterialList(rootNode + "obstacleBlocks");
}
@Override
@ -89,11 +89,14 @@ public class ParkourConfiguration extends MiniGameConfiguration {
"Current configuration:" +
"\n" + "Must do groups in sequence: " + mustDoGroupedInSequence +
"\n" + "Ignore records until group beaten once: " + ignoreRecordsUntilGroupBeatenOnce +
"\n" + "Make players invisible: " + makePlayersInvisible +
"\n" + "Kill plane blocks: ");
for (Material material : killPlaneBlocks) {
builder.append("\n - ").append(material.name());
}
builder.append("\n" + "Obstacle blocks: ");
for (Material material : obstacleBlocks) {
builder.append("\n - ").append(material.name());
}
return builder.toString();
}

View File

@ -0,0 +1,266 @@
package net.knarcraft.minigames.gui;
import net.knarcraft.knargui.AbstractGUI;
import net.knarcraft.knargui.GUIAction;
import net.knarcraft.knargui.item.GUIItemFactory;
import net.knarcraft.knargui.item.PlayerHeadGUIItemFactory;
import net.knarcraft.knargui.item.SimpleGUIItemFactory;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.PlayerVisibilityManager;
import net.knarcraft.minigames.arena.parkour.ParkourArenaSession;
import net.knarcraft.minigames.util.GeyserHelper;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A generic GUI for all arenas
*/
public abstract class ArenaGUI extends AbstractGUI {
/**
* Instantiates a new arena gui
*
* @param inventorySize <p>The size of the GUI's inventory</p>
* @param inventoryName <p>The name of the inventory</p>
*/
public ArenaGUI(int inventorySize, @NotNull String inventoryName) {
super(inventorySize, inventoryName, null);
}
/**
* Gets an item describing a retry arena action (for Java edition)
*
* @return <p>An arena restart item</p>
*/
@NotNull
protected ItemStack getRestartItemJava() {
PlayerHeadGUIItemFactory restartItemFactory = new PlayerHeadGUIItemFactory();
restartItemFactory.useSkin("e23b225ed0443c4eec7cf30a034490485904e6eb3d53ef2ab9e39c73bdf22c30");
return setRestartItemData(restartItemFactory);
}
/**
* Gets an item describing a retry arena action (for Bedrock edition)
*
* @return <p>An arena restart item</p>
*/
@NotNull
protected ItemStack getRestartItemBedrock() {
return setRestartItemData(new SimpleGUIItemFactory(Material.MAGENTA_GLAZED_TERRACOTTA));
}
/**
* Sets the lore and name for a retry item, and returns the finished item
*
* @param guiItemFactory <p>The factory to apply the data to</p>
* @return <p>The finished item, with the data applied</p>
*/
@NotNull
protected ItemStack setRestartItemData(@NotNull GUIItemFactory guiItemFactory) {
List<String> loreLines = getLoreLines();
loreLines.add(ChatColor.GRAY + "Use this item to retry the arena");
loreLines.add(ChatColor.GRAY + "(deaths and time is reset to 0)");
guiItemFactory.setName(ChatColor.BLUE + "Retry arena");
guiItemFactory.setLore(loreLines);
return guiItemFactory.build();
}
/**
* Gets an item describing player visibility toggling
*
* @return <p>A player toggle item</p>
*/
@NotNull
protected ItemStack getTogglePlayersItemDisabled() {
GUIItemFactory togglePlayersItemFactory = new SimpleGUIItemFactory(Material.PLAYER_HEAD);
List<String> loreLines = getLoreLines();
loreLines.add(ChatColor.GRAY + "Use this item to disable the visibility");
loreLines.add(ChatColor.GRAY + "of other players");
togglePlayersItemFactory.setName(ChatColor.BLUE + "Disable Players");
togglePlayersItemFactory.setLore(loreLines);
return togglePlayersItemFactory.build();
}
/**
* Gets an item describing player visibility toggling
*
* @param player <p>The player to get the item for</p>
* @return <p>A player toggle item</p>
*/
@NotNull
protected ItemStack getTogglePlayersItemEnabled(@NotNull Player player) {
if (GeyserHelper.isGeyserPlayer(player)) {
return getTogglePlayersItemEnabledBedrock();
} else {
return getTogglePlayersItemEnabledJava();
}
}
/**
* Gets an item describing player visibility toggling
*
* @return <p>A player toggle item</p>
*/
@NotNull
protected ItemStack getTogglePlayersItemEnabledJava() {
PlayerHeadGUIItemFactory togglePlayersItemFactory = new PlayerHeadGUIItemFactory();
togglePlayersItemFactory.useSkin("c10591e6909e6a281b371836e462d67a2c78fa0952e910f32b41a26c48c1757c");
return setTogglePlayersItemData(togglePlayersItemFactory);
}
/**
* Gets an item describing player visibility toggling
*
* @return <p>A player toggle item</p>
*/
@NotNull
protected ItemStack getTogglePlayersItemEnabledBedrock() {
return setTogglePlayersItemData(new SimpleGUIItemFactory(Material.SKELETON_SKULL));
}
/**
* Sets the lore and name for a toggle players item, and returns the finished item
*
* @param itemFactory <p>The factory to apply the data to</p>
* @return <p>The finished item, with the data applied</p>
*/
@NotNull
protected ItemStack setTogglePlayersItemData(@NotNull GUIItemFactory itemFactory) {
List<String> loreLines = getLoreLines();
loreLines.add(ChatColor.GRAY + "Use this item to enable the visibility");
loreLines.add(ChatColor.GRAY + "of other players");
itemFactory.setName(ChatColor.BLUE + "Enable Players");
itemFactory.setLore(loreLines);
return itemFactory.build();
}
/**
* Gets an item describing a give up action
*
* @return <p>A give up item</p>
*/
@NotNull
protected ItemStack getGiveUpItem() {
GUIItemFactory giveUpItemFactory = new SimpleGUIItemFactory(Material.RESPAWN_ANCHOR);
List<String> loreLines = getLoreLines();
loreLines.add(ChatColor.GRAY + "Use this item to give up and");
loreLines.add(ChatColor.GRAY + "go to your current checkpoint");
giveUpItemFactory.setName(ChatColor.RED + "Give up");
giveUpItemFactory.setLore(loreLines);
return giveUpItemFactory.build();
}
/**
* Gets an item describing a leave arena action
*
* @return <p>A leave item</p>
*/
@NotNull
protected ItemStack getLeaveItem() {
GUIItemFactory leaveItemFactory = new SimpleGUIItemFactory(Material.BARRIER);
List<String> loreLines = getLoreLines();
loreLines.add(ChatColor.GRAY + "Use this item to leave the arena");
leaveItemFactory.setName(ChatColor.DARK_RED + "Leave");
leaveItemFactory.setLore(loreLines);
return leaveItemFactory.build();
}
/**
* Gets an arraylist with one blank line lore-lines can be added to
*
* @return <p>An arraylist with one blank line</p>
*/
@NotNull
protected List<String> getLoreLines() {
List<String> loreLines = new ArrayList<>();
loreLines.add("");
return loreLines;
}
/**
* Sets a click action for both right-click and left-click
*
* @param inventorySlot <p>The inventory slot the action should be added to</p>
* @param action <p>The action to register</p>
*/
protected void setAnyClickAction(int inventorySlot, @NotNull GUIAction action) {
setClickAction(inventorySlot, ClickType.LEFT, action);
setClickAction(inventorySlot, ClickType.RIGHT, action);
}
/**
* Gets the action to run when triggering the leave item
*
* @return <p>The leave action</p>
*/
@NotNull
public static GUIAction getLeaveAction() {
return (player) -> {
ArenaSession session = MiniGames.getInstance().getSession(player.getUniqueId());
if (session != null) {
session.triggerQuit(false, true);
}
};
}
/**
* Gets the action to run when triggering the restart action
*
* @return <p>The action for triggering a session restart</p>
*/
@NotNull
public static GUIAction getRestartAction() {
return (player -> {
ArenaSession session = MiniGames.getInstance().getSession(player.getUniqueId());
if (session != null) {
session.reset();
}
});
}
/**
* Gets the action to run when triggering the toggle players action
*
* @param playerRegistry <p>The registry containing relevant players</p>
* @param inventorySlot <p>The inventory slot to replace when toggling</p>
* @return <p>The action for triggering player visibility</p>
*/
@NotNull
public GUIAction getTogglePlayersAction(@Nullable ArenaPlayerRegistry<?> playerRegistry, int inventorySlot) {
return (player) -> {
PlayerVisibilityManager visibilityManager = MiniGames.getInstance().getPlayerVisibilityManager();
visibilityManager.toggleHidePlayers(playerRegistry, player);
if (MiniGames.getInstance().getPlayerVisibilityManager().isHidingPlayers(player)) {
setItem(inventorySlot, getTogglePlayersItemEnabled(player));
} else {
setItem(inventorySlot, getTogglePlayersItemDisabled());
}
};
}
/**
* Gets the action to run when triggering the give up item
*
* @return <p>The give up action</p>
*/
@NotNull
public static GUIAction getGiveUpAction() {
return (player) -> {
ArenaSession session = MiniGames.getInstance().getSession(player.getUniqueId());
if (session instanceof ParkourArenaSession) {
session.triggerLoss();
}
};
}
}

View File

@ -0,0 +1,31 @@
package net.knarcraft.minigames.gui;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.entity.Player;
/**
* A GUI used in the dropper arena
*/
public class DropperGUI extends ArenaGUI {
/**
* Instantiates a new dropper gui
*
* @param player <p>The player the GUI is created for</p>
*/
public DropperGUI(Player player) {
super(9, "Dropper");
if (MiniGames.getInstance().getPlayerVisibilityManager().isHidingPlayers(player)) {
setItem(0, getTogglePlayersItemEnabled(player));
} else {
setItem(0, getTogglePlayersItemDisabled());
}
setItem(2, getLeaveItem());
setItem(4, getRestartItemJava());
setAnyClickAction(0, getTogglePlayersAction(MiniGames.getInstance().getDropperArenaPlayerRegistry(), 0));
setAnyClickAction(2, getLeaveAction());
setAnyClickAction(4, getRestartAction());
}
}

View File

@ -0,0 +1,31 @@
package net.knarcraft.minigames.gui;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.entity.Player;
/**
* A GUI used in the dropper arena, altered for Bedrock players
*/
public class DropperGUIBedrock extends ArenaGUI {
/**
* Instantiates a new dropper gui
*
* @param player <p>The player the GUI is created for</p>
*/
public DropperGUIBedrock(Player player) {
super(27, "Dropper");
if (MiniGames.getInstance().getPlayerVisibilityManager().isHidingPlayers(player)) {
setItem(10, getTogglePlayersItemEnabledBedrock());
} else {
setItem(10, getTogglePlayersItemDisabled());
}
setItem(12, getLeaveItem());
setItem(14, getRestartItemBedrock());
setAnyClickAction(10, getTogglePlayersAction(MiniGames.getInstance().getDropperArenaPlayerRegistry(), 10));
setAnyClickAction(12, getLeaveAction());
setAnyClickAction(14, getRestartAction());
}
}

View File

@ -0,0 +1,27 @@
package net.knarcraft.minigames.gui;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.entity.Player;
/**
* A GUI used outside arenas
*/
public class MiniGamesGUI extends ArenaGUI {
/**
* Instantiates a new mini games gui
*
* @param player <p>The player the GUI is created for</p>
*/
public MiniGamesGUI(Player player) {
super(9, "MiniGames");
if (MiniGames.getInstance().getPlayerVisibilityManager().isHidingPlayers(player)) {
setItem(0, getTogglePlayersItemEnabled(player));
} else {
setItem(0, getTogglePlayersItemDisabled());
}
setAnyClickAction(0, getTogglePlayersAction(null, 0));
}
}

View File

@ -0,0 +1,33 @@
package net.knarcraft.minigames.gui;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.entity.Player;
/**
* A GUI used in the parkour arena
*/
public class ParkourGUI extends ArenaGUI {
/**
* Instantiates a new parkour gui
*
* @param player <p>The player the GUI is created for</p>
*/
public ParkourGUI(Player player) {
super(9, "Parkour");
if (MiniGames.getInstance().getPlayerVisibilityManager().isHidingPlayers(player)) {
setItem(1, getTogglePlayersItemEnabled(player));
} else {
setItem(1, getTogglePlayersItemDisabled());
}
setItem(3, getGiveUpItem());
setItem(5, getLeaveItem());
setItem(7, getRestartItemJava());
setAnyClickAction(1, getTogglePlayersAction(MiniGames.getInstance().getParkourArenaPlayerRegistry(), 1));
setAnyClickAction(3, getGiveUpAction());
setAnyClickAction(5, getLeaveAction());
setAnyClickAction(7, getRestartAction());
}
}

View File

@ -0,0 +1,33 @@
package net.knarcraft.minigames.gui;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.entity.Player;
/**
* A GUI used in the parkour arena, altered for Bedrock players
*/
public class ParkourGUIBedrock extends ArenaGUI {
/**
* Instantiates a new parkour gui
*
* @param player <p>The player the GUI is created for</p>
*/
public ParkourGUIBedrock(Player player) {
super(27, "Parkour");
if (MiniGames.getInstance().getPlayerVisibilityManager().isHidingPlayers(player)) {
setItem(10, getTogglePlayersItemEnabled(player));
} else {
setItem(10, getTogglePlayersItemDisabled());
}
setItem(12, getGiveUpItem());
setItem(14, getLeaveItem());
setItem(16, getRestartItemBedrock());
setAnyClickAction(10, getTogglePlayersAction(MiniGames.getInstance().getParkourArenaPlayerRegistry(), 10));
setAnyClickAction(12, getGiveUpAction());
setAnyClickAction(14, getLeaveAction());
setAnyClickAction(16, getRestartAction());
}
}

View File

@ -2,6 +2,7 @@ package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.config.MiniGameMessage;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -28,6 +29,8 @@ public class CommandListener implements Listener {
allowedCommands.add("/mLeave");
allowedCommands.add("/dLeave");
allowedCommands.add("/pLeave");
allowedCommands.add("/miniGamesMenu");
allowedCommands.add("/mMenu");
String message = event.getMessage();
if (!message.startsWith("/")) {
@ -40,7 +43,7 @@ public class CommandListener implements Listener {
}
}
player.sendMessage("You cannot use that command while in an arena!");
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player, MiniGameMessage.ERROR_ILLEGAL_COMMAND);
event.setCancelled(true);
}

View File

@ -1,8 +1,12 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.knarlib.lib.annotations.NotNull;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import org.bukkit.Sound;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -16,7 +20,7 @@ import org.bukkit.event.entity.EntityDamageEvent;
public class DamageListener implements Listener {
@EventHandler
public void onPlayerDamage(EntityDamageEvent event) {
public void onPlayerDamage(@NotNull EntityDamageEvent event) {
// Only player damage matters
if (event.getEntityType() != EntityType.PLAYER) {
return;
@ -32,14 +36,52 @@ public class DamageListener implements Listener {
event.setCancelled(true);
// Only trigger a loss when a player suffers fall damage in a dropper arena
// Only trigger a loss when a player suffers fall damage in a dropper arena (This cannot be cancelled!)
if (arenaSession instanceof DropperArenaSession && event.getCause() == EntityDamageEvent.DamageCause.FALL) {
arenaSession.triggerLoss();
return;
}
// If set as allowed damage, do nothing, except if the damage is fatal
if (arenaSession.getArena().getAllowedDamageCauses().contains(event.getCause())) {
applyFakeDamage(player, event.getDamage(), arenaSession);
return;
}
// If set as trigger loss, trigger a loss
if (arenaSession.getArena().getLossTriggerDamageCauses().contains(event.getCause())) {
arenaSession.triggerLoss();
}
}
/**
* Fakes the damaging of a player
*
* @param player <p>The player to damage</p>
* @param damage <p>The raw damage to apply</p>
* @param arenaSession <p>The arena session to trigger a loss for, if the player reaches 0 damage</p>
*/
private void applyFakeDamage(@NotNull Player player, double damage, @NotNull ArenaSession arenaSession) {
if (player.getNoDamageTicks() > 0) {
return;
}
double newHealth = player.getHealth() - damage;
player.sendHurtAnimation(180);
if (newHealth <= 0) {
AttributeInstance health = player.getAttribute(Attribute.MAX_HEALTH);
if (health != null) {
player.setHealth(health.getValue());
}
arenaSession.triggerLoss();
} else {
player.setHealth(newHealth);
player.setNoDamageTicks(10);
player.playSound(player, Sound.ENTITY_PLAYER_HURT, 1, 1);
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerCombustion(EntityCombustEvent event) {
public void onPlayerCombustion(@NotNull EntityCombustEvent event) {
if (event.getEntityType() != EntityType.PLAYER) {
return;
}

View File

@ -0,0 +1,82 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaSession;
import net.knarcraft.minigames.gui.ArenaGUI;
import net.knarcraft.minigames.gui.ParkourGUI;
import net.knarcraft.minigames.property.PersistentDataKey;
import net.knarcraft.minigames.util.GUIHelper;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Switch;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
/**
* A listener that listens for player interactions
*/
public class InteractListener implements Listener {
@EventHandler
public void menuInteractListener(@NotNull PlayerInteractEvent event) {
handleMenuClick(event);
Block clicked = event.getClickedBlock();
if (event.useInteractedBlock() != Event.Result.DENY && event.getAction() == Action.RIGHT_CLICK_BLOCK &&
clicked != null && clicked.getBlockData() instanceof Switch) {
ArenaPlayerRegistry<ParkourArena> playerRegistry = MiniGames.getInstance().getParkourArenaPlayerRegistry();
ParkourArenaSession arenaSession = (ParkourArenaSession) playerRegistry.getArenaSession(event.getPlayer().getUniqueId());
if (arenaSession != null) {
arenaSession.registerChangedLever(clicked);
}
}
}
/**
* Handles clicking of the menu item
*
* @param event <p>The triggered player interact event</p>
*/
private void handleMenuClick(@NotNull PlayerInteractEvent event) {
ItemStack item = event.getItem();
if (item == null || !item.hasItemMeta()) {
return;
}
ItemMeta meta = item.getItemMeta();
if (meta == null) {
return;
}
Integer persistentData = meta.getPersistentDataContainer().get(new NamespacedKey(MiniGames.getInstance(),
PersistentDataKey.MENU_ITEM.getKeyName()), PersistentDataType.INTEGER);
if (persistentData != null && persistentData == PersistentDataKey.MENU_ITEM.getDataValue()) {
event.setUseItemInHand(Event.Result.DENY);
event.setUseInteractedBlock(Event.Result.DENY);
GUIHelper.openGUI(event.getPlayer());
return;
}
persistentData = meta.getPersistentDataContainer().get(new NamespacedKey(MiniGames.getInstance(),
PersistentDataKey.LEAVE_ITEM.getKeyName()), PersistentDataType.INTEGER);
if (persistentData != null) {
event.setUseItemInHand(Event.Result.DENY);
event.setUseInteractedBlock(Event.Result.DENY);
if (persistentData == PersistentDataKey.LEAVE_ITEM.getDataValue()) {
ArenaGUI.getLeaveAction().run(event.getPlayer());
} else if (persistentData == PersistentDataKey.GIVE_UP_ITEM.getDataValue()) {
ParkourGUI.getGiveUpAction().run(event.getPlayer());
}
}
}
}

View File

@ -3,19 +3,25 @@ package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGameMode;
import net.knarcraft.minigames.arena.parkour.ParkourArenaSession;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.config.SharedConfiguration;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
@ -23,12 +29,15 @@ import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
/**
* A listener for players moving inside a dropper arena
*/
public class MoveListener implements Listener {
private static final BoundingBox fullBlockBox = new BoundingBox(0, 0, 0, 1, 1, 1);
private final DropperConfiguration dropperConfiguration;
private final ParkourConfiguration parkourConfiguration;
@ -46,12 +55,15 @@ public class MoveListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// Ignore if no actual movement is happening
if (event.getFrom().equals(event.getTo()) || event.getTo() == null) {
if (event.getTo() == null) {
return;
}
ArenaSession session = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId());
if (session instanceof DropperArenaSession dropperSession) {
if (event.getFrom().equals(event.getTo())) {
return;
}
doDropperArenaChecks(event, dropperSession);
} else if (session instanceof ParkourArenaSession parkourSession) {
doParkourArenaChecks(event, parkourSession);
@ -65,8 +77,7 @@ public class MoveListener implements Listener {
* @param arenaSession <p>The dropper session of the player triggering the event</p>
*/
private void doParkourArenaChecks(@NotNull PlayerMoveEvent event, ParkourArenaSession arenaSession) {
// Ignore movement which won't cause the player's block to change
if (event.getTo() == null || event.getFrom().getBlock() == event.getTo().getBlock()) {
if (event.getTo() == null) {
return;
}
@ -75,26 +86,55 @@ public class MoveListener implements Listener {
return;
}
// Skip checkpoint registration if playing on hardcore
if (arenaSession.getGameMode() == ParkourArenaGameMode.HARDCORE) {
return;
}
// Check if the player reached one of the checkpoints for the arena
updateCheckpoint(arenaSession, event.getTo().getBlock(), event.getPlayer());
}
/**
* Updates the checkpoint of a player if reached
*
* @param arenaSession <p>The session of the player</p>
* @param targetBlock <p>The block the player is moving to</p>
* @param player <p>The player moving</p>
*/
private void updateCheckpoint(@NotNull ParkourArenaSession arenaSession, @NotNull Block targetBlock,
@NotNull Player player) {
ParkourArena arena = arenaSession.getArena();
List<Location> checkpoints = arena.getCheckpoints();
for (Location checkpoint : checkpoints) {
Location previousCheckpoint = arenaSession.getRegisteredCheckpoint();
if (checkpoint.getBlock().equals(event.getTo().getBlock()) && !checkpoint.equals(previousCheckpoint)) {
// Skip if checkpoint has not been reached
if (!checkpoint.getBlock().equals(targetBlock)) {
continue;
}
// If the checkpoint is the same as the previously reached one, abort
if (previousCheckpoint != null && checkpoint.getBlock().equals(previousCheckpoint.getBlock())) {
return;
}
// If not the correct checkpoint according to the enforced order, abort
if (parkourConfiguration.enforceCheckpointOrder()) {
int checkpointIndex = checkpoints.indexOf(checkpoint);
int previousIndex = previousCheckpoint == null ? -1 : checkpoints.indexOf(previousCheckpoint);
if (checkpointIndex - previousIndex != 1) {
continue;
return;
}
}
// Register the checkpoint
arenaSession.registerCheckpoint(checkpoint.clone());
event.getPlayer().sendMessage("Checkpoint reached!");
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(player,
MiniGameMessage.SUCCESS_CHECKPOINT_REACHED);
return;
}
}
}
/**
* Performs the necessary checks and tasks for the player's session
@ -103,14 +143,26 @@ public class MoveListener implements Listener {
* @param arenaSession <p>The dropper session of the player triggering the event</p>
*/
private void doDropperArenaChecks(@NotNull PlayerMoveEvent event, @NotNull DropperArenaSession arenaSession) {
if (event.getTo() == null) {
// If the player has yet to move in the arena, allow them to look around
boolean startedMoving = arenaSession.getStartedMoving();
if (event.getTo() == null ||
(!startedMoving && isSameLocation(event.getFrom(), event.getTo()))) {
return;
}
// Marks the player as started moving if necessary, so they can no longer hang in the air
if (!startedMoving) {
arenaSession.setStartedMoving();
}
// Prevent the player from flying upwards while in flight mode
if (event.getFrom().getY() < event.getTo().getY() ||
(dropperConfiguration.blockSneaking() && event.getPlayer().isSneaking()) ||
(dropperConfiguration.blockSprinting() && event.getPlayer().isSprinting())) {
event.setCancelled(true);
// Force movement downwards once the player lets go
Bukkit.getScheduler().scheduleSyncDelayedTask(MiniGames.getInstance(),
() -> updatePlayerVelocity(arenaSession), 1);
return;
}
@ -125,6 +177,18 @@ public class MoveListener implements Listener {
updatePlayerVelocity(arenaSession);
}
/**
* Checks if two locations are the same, excluding rotation
*
* @param location1 <p>The first location to check</p>
* @param location2 <p>The second location to check</p>
* @return <p>True if the locations are the same, excluding rotation</p>
*/
private boolean isSameLocation(Location location1, Location location2) {
return location1.getX() == location2.getX() && location1.getY() == location2.getY() &&
location1.getZ() == location2.getZ();
}
/**
* Check if the player in the session is triggering a block with a special significance
*
@ -134,30 +198,199 @@ public class MoveListener implements Listener {
* @param toLocation <p>The location the player's session is about to hit</p>
* @return <p>True if a special block has been hit</p>
*/
private boolean checkForSpecialBlock(ArenaSession arenaSession, Location toLocation) {
private boolean checkForSpecialBlock(@NotNull ArenaSession arenaSession, @NotNull Location toLocation) {
SharedConfiguration sharedConfiguration = MiniGames.getInstance().getSharedConfiguration();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
double solidDepth = sharedConfiguration.getSolidHitBoxDistance();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
Arena arena = arenaSession.getArena();
if (arena instanceof DropperArena) {
return checkDropperWin(arenaSession, toLocation, solidDepth, liquidDepth) ||
checkDropperDeathBlocks((DropperArenaSession) arenaSession, toLocation, solidDepth, liquidDepth);
} else if (arena instanceof ParkourArena) {
return checkParkourWin(arenaSession, toLocation.getBlock()) ||
checkParkourDeathBlock((ParkourArenaSession) arenaSession, toLocation);
} else {
MiniGames.log(Level.SEVERE, "Unknown arena type encountered!");
}
return false;
}
/**
* Checks if the player has triggered a parkour win block
*
* @param arenaSession <p>The player's current arena session</p>
* @param targetBlock <p>The block the player moved to</p>
* @return <p>True if the player triggered a parkour win block</p>
*/
private boolean checkParkourWin(@NotNull ArenaSession arenaSession, @NotNull Block targetBlock) {
boolean winBlock = arenaSession.getArena().willCauseWin(targetBlock);
if (winBlock) {
arenaSession.triggerWin();
}
return winBlock;
}
/**
* Checks if the player has triggered a dropper win block
*
* @param arenaSession <p>The player's arena session</p>
* @param toLocation <p>The location the player moved to</p>
* @param solidDepth <p>The distance away from solid blocks that will trigger a hit</p>
* @param liquidDepth <p>The depth players need to be inside a liquid to trigger a hit</p>
* @return <p>True if the player triggered a dropper win block</p>
*/
private boolean checkDropperWin(@NotNull ArenaSession arenaSession, @NotNull Location toLocation, double solidDepth,
double liquidDepth) {
Arena arena = arenaSession.getArena();
Set<Block> potentialWinTriggerBlocks;
// For water, only trigger when the player enters the water, but trigger earlier for everything else
double depth = arena.winLocationIsSolid() ? solidDepth : liquidDepth;
for (Block block : getBlocksBeneathLocation(toLocation, depth)) {
if (arena.winLocationIsSolid()) {
potentialWinTriggerBlocks = getBlocksBeneathLocation(toLocation, solidDepth);
} else {
potentialWinTriggerBlocks = getBlocksBeneathLocation(toLocation, liquidDepth);
}
for (Block block : potentialWinTriggerBlocks) {
if (arena.willCauseWin(block)) {
arenaSession.triggerWin();
return true;
}
}
return false;
}
/**
* Checks if a player is moving onto a block part of the dropper arena death plane
*
* @param arenaSession <p>The player's arena session</p>
* @param toLocation <p>The location the player is moving to</p>
* @param solidDepth <p>The distance away from solid blocks that will trigger a hit</p>
* @param liquidDepth <p>The depth players need to be inside a liquid to trigger a hit</p>
* @return <p>True if the player triggered a dropper death block</p>
*/
private boolean checkDropperDeathBlocks(@NotNull DropperArenaSession arenaSession,
@NotNull Location toLocation, double solidDepth, double liquidDepth) {
Arena arena = arenaSession.getArena();
// Check if the player is about to hit a non-air and non-liquid block
for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) {
if (!block.getType().isAir() && arena.willCauseLoss(block)) {
if (!block.getType().isAir() && !block.isLiquid() && arena.willCauseLoss(block)) {
arenaSession.triggerLoss();
return true;
}
}
// Check if the player has entered a liquid that causes a loss
for (Block block : getBlocksBeneathLocation(toLocation, liquidDepth)) {
if (block.isLiquid() && arena.willCauseLoss(block)) {
arenaSession.triggerLoss();
return true;
}
}
return false;
}
/**
* Checks if a player is moving onto a block part of the parkour death plane
*
* @param arenaSession <p>The player's arena session</p>
* @param toLocation <p>The location the player is moving to</p>
* @return <p>True if the player hit a death block</p>
*/
private boolean checkParkourDeathBlock(@NotNull ParkourArenaSession arenaSession,
@NotNull Location toLocation) {
// A simple check, only for kill blocks
if (isOnKillBlock(arenaSession, toLocation)) {
return true;
}
// As the check for obstacle blocks is extensive, it's skipped if possible
Set<Material> obstacleBlocks = arenaSession.getArena().getObstacleBlocks();
if (obstacleBlocks.isEmpty()) {
return false;
}
// Create a hit-box approximate to the player's real hit-box
double playerHeight = 1.8;
Player player = Bukkit.getPlayer(arenaSession.getEntryState().getPlayerId());
if (player != null && player.isSneaking()) {
playerHeight = 1.5;
}
BoundingBox playerBox = new BoundingBox(-0.05, -0.05, -0.05,
0.6 + 0.05, playerHeight + 0.05, 0.6 + 0.05).shift(
toLocation).shift(-0.3, -0.05, -0.3);
BoundingBox playerPassableBox = new BoundingBox(0.2, 0.5, 0.2,
0.4, playerHeight - 0.5, 0.4).shift(
toLocation).shift(-0.3, 0, -0.3);
Set<Block> possiblyHitBlocks = new HashSet<>();
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, 0, 0.01));
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, 1, 0.01));
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, -1, 0.01));
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, -2, 0.01));
for (Block block : possiblyHitBlocks) {
if (!obstacleBlocks.contains(block.getType())) {
continue;
}
// For liquids, or anything without a proper collision shape, trigger collision if the player is partly
// inside when treated as a full block
if (block.isLiquid() || block.getCollisionShape().getBoundingBoxes().isEmpty()) {
if (playerPassableBox.overlaps(fullBlockBox.clone().shift(block.getLocation()))) {
arenaSession.triggerLoss();
return true;
}
// Make the hit-box stricter for liquids beside the player to make lava labyrinths feasible
if (block.isLiquid() && player != null && block.getY() > player.getLocation().getBlockY() &&
playerBox.overlaps(fullBlockBox.clone().shift(block.getLocation()))) {
arenaSession.triggerLoss();
return true;
}
}
// Check whether the player's actual hit-box is intersecting with a block
for (BoundingBox boundingBox : block.getCollisionShape().getBoundingBoxes()) {
// A collision shape's bounding box is relative to 0,0 and therefore must be adjusted to the block's
// location. Then overlap is checked by the player's collision box and the shifted bounding box.
if (playerBox.overlaps(boundingBox.clone().shift(block.getLocation()))) {
arenaSession.triggerLoss();
return true;
}
}
}
return false;
}
/**
* As simple check for whether a player is moving on top of a kill block
*
* @param arenaSession <p>The arena session the player is in</p>
* @param toLocation <p>The location the player is moving to</p>
* @return <p>True if the player is on a kill block, and a loss has been triggered</p>
*/
private boolean isOnKillBlock(ParkourArenaSession arenaSession, Location toLocation) {
// If the player is standing on a non-full block, event.getTo will give the correct block, but if not, the
// block below has to be checked instead.
Set<Block> blocksBelow = getBlocksBeneathLocation(toLocation, 0);
Set<Material> killPlaneBlocks = arenaSession.getArena().getKillPlaneBlocks();
for (Block block : blocksBelow) {
if (block.getType().isAir()) {
block = block.getLocation().clone().subtract(0, 0.2, 0).getBlock();
// Only trigger hit detection for passable blocks if the player is in the block
if (block.isPassable()) {
continue;
}
}
if (killPlaneBlocks.contains(block.getType())) {
arenaSession.triggerLoss();
return true;
}
}
return false;
}
@ -168,12 +401,31 @@ public class MoveListener implements Listener {
* @return <p>The blocks beneath the player</p>
*/
private Set<Block> getBlocksBeneathLocation(Location location, double depth) {
return getBlocksBeneathLocation(location, depth, 0);
}
/**
* Gets the blocks at the given location that will be affected by the player's hit-box
*
* @param location <p>The location to check</p>
* @param extraRange <p>Extra range of the square used for finding blocks</p>
* @return <p>The blocks beneath the player</p>
*/
private Set<Block> getBlocksBeneathLocation(Location location, double depth, double extraRange) {
Set<Block> blocksBeneath = new HashSet<>();
double halfPlayerWidth = 0.3;
double halfPlayerWidth = 0.3 + extraRange;
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
// Once a certain size is reached, if the player is in the centre of a block, 9 must be accounted for
if (halfPlayerWidth > 0.5) {
blocksBeneath.add(location.getBlock());
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, 0).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, 0).getBlock());
blocksBeneath.add(location.clone().subtract(0, depth, -halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(0, depth, halfPlayerWidth).getBlock());
}
return blocksBeneath;
}

View File

@ -1,79 +0,0 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.parkour.ParkourArenaSession;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A listener for players leaving the server or the arena
*/
public class PlayerLeaveListener implements Listener {
private final Map<UUID, ArenaSession> leftSessions = new HashMap<>();
@EventHandler
public void onPlayerLeave(PlayerQuitEvent event) {
Player player = event.getPlayer();
ArenaSession arenaSession = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId());
if (arenaSession == null) {
return;
}
MiniGames.log(Level.WARNING, "Found player " + player.getUniqueId() +
" leaving in the middle of a session!");
leftSessions.put(player.getUniqueId(), arenaSession);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
UUID playerId = event.getPlayer().getUniqueId();
// Force the player to quit from the session once they re-join
if (leftSessions.containsKey(playerId)) {
MiniGames.log(Level.WARNING, "Found un-exited dropper session!");
Bukkit.getScheduler().runTaskLater(MiniGames.getInstance(), () -> {
leftSessions.get(playerId).triggerQuit(false);
MiniGames.log(Level.WARNING, "Triggered a quit!");
leftSessions.remove(playerId);
}, 80);
}
}
@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) {
Location targetLocation = event.getTo();
if (targetLocation == null || event.isCancelled()) {
return;
}
ArenaSession arenaSession = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId());
if (arenaSession == null) {
return;
}
if (targetLocation.equals(arenaSession.getArena().getSpawnLocation())) {
return;
}
if (arenaSession instanceof ParkourArenaSession parkourArenaSession &&
targetLocation.equals(parkourArenaSession.getRegisteredCheckpoint())) {
return;
}
arenaSession.triggerQuit(false);
}
}

View File

@ -0,0 +1,92 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.PlayerEntryState;
import net.knarcraft.minigames.arena.parkour.ParkourArenaSession;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.spigotmc.event.player.PlayerSpawnLocationEvent;
import java.util.logging.Level;
/**
* A listener for players leaving/joining the server, or leaving the server unexpectedly
*/
public class PlayerStateChangeListener implements Listener {
@EventHandler
public void onPlayerSpawn(PlayerSpawnLocationEvent event) {
Player player = event.getPlayer();
// Restore any lingering arena states
Location restoreLocation;
restoreLocation = restoreStateIfNecessary(player, MiniGames.getInstance().getDropperArenaPlayerRegistry());
if (restoreLocation != null) {
event.setSpawnLocation(restoreLocation);
}
restoreLocation = restoreStateIfNecessary(player, MiniGames.getInstance().getParkourArenaPlayerRegistry());
if (restoreLocation != null) {
event.setSpawnLocation(restoreLocation);
}
}
/**
* Prevent the player from teleporting away from an arena for any reason
*
* @param event <p>The triggered teleport event</p>
*/
@EventHandler(ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent event) {
Location targetLocation = event.getTo();
if (targetLocation == null) {
return;
}
// Ignore if not in an arena session
ArenaSession arenaSession = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId());
if (arenaSession == null) {
return;
}
// If teleported to the arena's spawn, it's fine
if (targetLocation.equals(arenaSession.getArena().getSpawnLocation())) {
return;
}
// If teleported to the arena's checkpoint, it's fine
if (arenaSession instanceof ParkourArenaSession parkourArenaSession &&
targetLocation.equals(parkourArenaSession.getRegisteredCheckpoint())) {
return;
}
event.setCancelled(true);
}
/**
* Restores the state of the given player if a lingering session is found in the given player registry
*
* @param player <p>The player whose state should be checked</p>
* @param playerRegistry <p>The registry to check for a lingering state</p>
* @return <p>The location the player should spawn in, or null if not restored</p>
*/
private Location restoreStateIfNecessary(Player player, ArenaPlayerRegistry<?> playerRegistry) {
PlayerEntryState entryState = playerRegistry.getEntryState(player.getUniqueId());
if (entryState != null) {
MiniGames.log(Level.INFO, "Found existing state for joining player " + player +
". Attempting to restore the player's state.");
playerRegistry.removePlayer(player.getUniqueId(), false);
Bukkit.getScheduler().runTaskLater(MiniGames.getInstance(), () -> entryState.restore(player), 1);
return entryState.getEntryLocation();
} else {
return null;
}
}
}

View File

@ -0,0 +1,55 @@
package net.knarcraft.minigames.manager;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.OfflinePlayer;
/**
* A manager that performs all Economy tasks
*/
public final class EconomyManager {
private static Economy economy;
private EconomyManager() {
}
/**
* Initializes the economy manager
*
* @param economy <p>The economy object to use for everything economy-related</p>
*/
public static void initialize(Economy economy) {
EconomyManager.economy = economy;
}
/**
* Checks whether the economy manager has been initialized
*
* @return <p>True if the economy manager has been initialized</p>
*/
public static boolean isInitialized() {
return EconomyManager.economy != null;
}
/**
* Formats the given amount of currency according to the economy plugin's format
*
* @param amount <p>The amount of currency to format</p>
* @return <p>The formatted string</p>
*/
public static String format(double amount) {
return economy.format(amount);
}
/**
* Deposits a given sum into the given player's account
*
* @param player <p>The player to deposit money to</p>
* @param sum <p>The amount of money to deposit</p>
*/
public static void deposit(OfflinePlayer player, double sum) {
economy.depositPlayer(player, sum);
}
}

View File

@ -0,0 +1,64 @@
package net.knarcraft.minigames.manager;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A manager that performs all Permission tasks
*/
public final class PermissionManager {
private static Permission permission;
private PermissionManager() {
}
/**
* Initializes the permission manager
*
* @param permission <p>The permission object to use for everything permission-related</p>
*/
public static void initialize(Permission permission) {
PermissionManager.permission = permission;
}
/**
* Checks whether the permission manager has been initialized
*
* @return <p>True if the permission manager has been initialized</p>
*/
public static boolean isInitialized() {
return PermissionManager.permission != null;
}
/**
* Grants a permanent permission to a player
*
* @param player <p>The player to grant the permission to</p>
* @param permissionNode <p>The permission node to grant to the player</p>
*/
public static void addPermission(@NotNull Player player, @NotNull String permissionNode, @Nullable World world) {
if (world != null) {
permission.playerAdd(world.getName(), player, permissionNode);
} else {
permission.playerAdd(player, permissionNode);
}
}
/**
* Checks whether the given player has the given permission
*
* @param player <p>The player to check</p>
* @param permissionNode <p>The permission node to check for</p>
* @param world <p>The world to check for the permission</p>
* @return <p>True if the player has the permission</p>
*/
public static boolean hasPermission(@NotNull Player player, @NotNull String permissionNode, @Nullable String world) {
return permission.playerHas(world, player, permissionNode);
}
}

View File

@ -0,0 +1,75 @@
package net.knarcraft.minigames.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.placeholder.parsing.PlayerPlaceholderParser;
import net.knarcraft.minigames.placeholder.parsing.RecordPlaceholderParser;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A placeholderAPI expansion for Dropper-related placeholders
*/
public class DropperExpansion extends PlaceholderExpansion {
private final @NotNull RecordPlaceholderParser recordPlaceholderParser;
private final @NotNull PlayerPlaceholderParser<DropperArena> playerPlaceholderParser;
/**
* Instantiates a new dropper expansion
*
* @param plugin <p>A reference to the mini-games plugin</p>
*/
public DropperExpansion(@NotNull MiniGames plugin) {
DropperArenaHandler arenaHandler = plugin.getDropperArenaHandler();
this.recordPlaceholderParser = new RecordPlaceholderParser(arenaHandler, DropperArenaGameMode::matchGameMode);
this.playerPlaceholderParser = new PlayerPlaceholderParser<>(arenaHandler, DropperArenaGameMode::matchGameMode,
plugin.getDropperArenaPlayerRegistry());
}
/**
* Clears record caches
*/
public void clearCaches() {
this.recordPlaceholderParser.clearCaches();
}
@Override
public String getIdentifier() {
return "dropper";
}
@Override
public String getAuthor() {
return "EpicKnarvik97";
}
@Override
public String getVersion() {
return "1.0.0";
}
@Override
public boolean persist() {
return true;
}
@Override
@Nullable
public String onRequest(OfflinePlayer player, String parameters) {
String[] parts = parameters.split("_");
// Record is used as the prefix for all record placeholders in case more placeholder types are added
if (parts[0].equalsIgnoreCase("record") && parts.length >= 7) {
return recordPlaceholderParser.onRequest(parts);
} else if (parts[0].equalsIgnoreCase("players")) {
return this.playerPlaceholderParser.onRequest(parts);
} else {
return parameters;
}
}
}

View File

@ -1,32 +0,0 @@
package net.knarcraft.minigames.placeholder;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import org.jetbrains.annotations.NotNull;
/**
* A placeholder expansion for dropper record placeholders
*/
public class DropperRecordExpansion extends RecordExpansion {
/**
* Initializes a new record expansion
*
* @param plugin <p>A reference to the dropper plugin</p>
*/
public DropperRecordExpansion(MiniGames plugin) {
super(plugin.getDropperArenaHandler());
}
@Override
public String getIdentifier() {
return "dropper";
}
@Override
protected @NotNull ArenaGameMode parseGameMode(@NotNull String gameMode) {
return DropperArenaGameMode.matchGamemode(gameMode);
}
}

View File

@ -0,0 +1,75 @@
package net.knarcraft.minigames.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGameMode;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.placeholder.parsing.PlayerPlaceholderParser;
import net.knarcraft.minigames.placeholder.parsing.RecordPlaceholderParser;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A placeholderAPI expansion for Parkour-related placeholders
*/
public class ParkourExpansion extends PlaceholderExpansion {
private final @NotNull RecordPlaceholderParser recordPlaceholderParser;
private final @NotNull PlayerPlaceholderParser<ParkourArena> playerPlaceholderParser;
/**
* Instantiates a new dropper expansion
*
* @param plugin <p>A reference to the mini-games plugin</p>
*/
public ParkourExpansion(@NotNull MiniGames plugin) {
ParkourArenaHandler arenaHandler = plugin.getParkourArenaHandler();
this.recordPlaceholderParser = new RecordPlaceholderParser(arenaHandler, ParkourArenaGameMode::matchGameMode);
this.playerPlaceholderParser = new PlayerPlaceholderParser<>(arenaHandler, ParkourArenaGameMode::matchGameMode,
plugin.getParkourArenaPlayerRegistry());
}
/**
* Clears record caches
*/
public void clearCaches() {
this.recordPlaceholderParser.clearCaches();
}
@Override
public String getIdentifier() {
return "parkour";
}
@Override
public String getAuthor() {
return "EpicKnarvik97";
}
@Override
public String getVersion() {
return "1.0.0";
}
@Override
public boolean persist() {
return true;
}
@Override
@Nullable
public String onRequest(OfflinePlayer player, String parameters) {
String[] parts = parameters.split("_");
// Record is used as the prefix for all record placeholders in case more placeholder types are added
if (parts[0].equalsIgnoreCase("record") && parts.length >= 7) {
return recordPlaceholderParser.onRequest(parts);
} else if (parts[0].equalsIgnoreCase("players")) {
return this.playerPlaceholderParser.onRequest(parts);
} else {
return parameters;
}
}
}

View File

@ -1,32 +0,0 @@
package net.knarcraft.minigames.placeholder;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.parkour.ParkourArenaGameMode;
import org.jetbrains.annotations.NotNull;
/**
* A placeholder expansion for parkour record placeholders
*/
public class ParkourRecordExpansion extends RecordExpansion {
/**
* Initializes a new record expansion
*
* @param plugin <p>A reference to the dropper plugin</p>
*/
public ParkourRecordExpansion(MiniGames plugin) {
super(plugin.getParkourArenaHandler());
}
@Override
public String getIdentifier() {
return "parkour";
}
@Override
protected @NotNull ArenaGameMode parseGameMode(@NotNull String gameMode) {
return ParkourArenaGameMode.matchGamemode(gameMode);
}
}

View File

@ -0,0 +1,37 @@
package net.knarcraft.minigames.placeholder.parsing;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The types of player information that can be retrieved in placeholders
*/
public enum PlayerInfoType {
/**
* The number of current players
*/
COUNT,
/**
* Information about a single player
*/
PLAYER,
;
/**
* Gets the info type specified in the given string
*
* @param type <p>The string specifying the info type</p>
* @return <p>The info type, or null if not found</p>
*/
public static @Nullable PlayerInfoType getFromString(@NotNull String type) {
for (PlayerInfoType infoType : PlayerInfoType.values()) {
if (infoType.name().equalsIgnoreCase(type)) {
return infoType;
}
}
return null;
}
}

View File

@ -0,0 +1,303 @@
package net.knarcraft.minigames.placeholder.parsing;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaGroup;
import net.knarcraft.minigames.arena.ArenaHandler;
import net.knarcraft.minigames.arena.ArenaPlayerRegistry;
import net.knarcraft.minigames.arena.ArenaSession;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.logging.Level;
/**
* A parser for player-related placeholders
*/
public class PlayerPlaceholderParser<K extends Arena> {
private final @NotNull ArenaHandler<K, ?> arenaHandler;
private final @NotNull Function<String, ArenaGameMode> gameModeParser;
private final ArenaPlayerRegistry<K> playerRegistry;
/**
* Initializes a new player placeholder parser
*
* @param arenaHandler <p>The arena handler to get arenas from</p>
* @param gameModeParser <p>The function to use for parsing the specified game-mode</p>
* @param playerRegistry <p>The player registry to get player info from</p>
*/
public PlayerPlaceholderParser(@NotNull ArenaHandler<K, ?> arenaHandler,
@NotNull Function<String, ArenaGameMode> gameModeParser,
ArenaPlayerRegistry<K> playerRegistry) {
this.arenaHandler = arenaHandler;
this.gameModeParser = gameModeParser;
this.playerRegistry = playerRegistry;
}
/**
* The method to run when parsing a record placeholder request
*
* @param parts <p>The split parameters, without irrelevant info</p>
* @return <p>The resulting string</p>
*/
@Nullable
public String onRequest(@NotNull String[] parts) {
if (parts.length < 2) {
return null;
}
String selector = parts[1];
if (parts.length >= 6 && selector.equalsIgnoreCase("playing")) {
return getPlayingPlayersInfo(parts);
} else if (parts.length >= 3 && selector.equalsIgnoreCase("maximum")) {
return getMaxPlayersInfo(parts);
} else {
return null;
}
}
/**
* Gets placeholder info about max players
*
* @param parts <p>The split parameters, without irrelevant info</p>
* @return <p>The resulting string</p>
*/
@Nullable
private String getMaxPlayersInfo(@NotNull String[] parts) {
String info = null;
K arena = arenaHandler.getArena(parts[2]);
if (arena != null) {
info = String.valueOf(arena.getMaxPlayers());
}
return info;
}
/**
* Gets placeholder info about playing players
*
* @param parts <p>The split parameters, without irrelevant info</p>
* @return <p>The resulting string</p>
*/
@Nullable
private String getPlayingPlayersInfo(@NotNull String[] parts) {
String gameModeName = parts[2];
ArenaGameMode gameMode = gameModeParser.apply(gameModeName);
if (gameModeName.equalsIgnoreCase("combined") || gameModeName.equalsIgnoreCase("all")) {
gameMode = null;
}
SelectionType selectionType = SelectionType.getFromString(parts[3]);
String identifier = parts[4];
// The type of info to get. Either count (number of players) or player_position (a named player).
PlayerInfoType infoType = PlayerInfoType.getFromString(parts[5]);
if (infoType == null) {
return null;
}
String info = null;
if (selectionType == SelectionType.GROUP) {
ArenaGroup<?, ?> group = arenaHandler.getGroup(identifier);
if (group != null) {
info = getGroupInfo(group, gameMode, infoType, parts);
}
} else if (selectionType == SelectionType.ARENA) {
info = getArenaInfo(identifier, gameMode, infoType, parts);
}
return info;
}
/**
* Gets information about an arena group's players
*
* @param group <p>The group to get info about</p>
* @param gameMode <p>The game-mode to get information for</p>
* @param infoType <p>The type of information to get</p>
* @param parts <p>The placeholder arguments specified by a user</p>
* @return <p>The specified group info, or null if the placeholder is invalid</p>
*/
@Nullable
private String getGroupInfo(@NotNull ArenaGroup<?, ?> group, @Nullable ArenaGameMode gameMode,
@NotNull PlayerInfoType infoType, @NotNull String[] parts) {
List<UUID> arenaIds = group.getArenas();
List<K> arenas = new ArrayList<>();
for (UUID arenaId : arenaIds) {
K arena = arenaHandler.getArena(arenaId);
if (arena != null) {
arenas.add(arena);
}
}
if (infoType == PlayerInfoType.COUNT) {
int playerCount = 0;
for (K arena : arenas) {
playerCount += getArenaPlayers(arena, gameMode).size();
}
return String.valueOf(playerCount);
} else if (infoType == PlayerInfoType.PLAYER) {
if (parts.length < 7) {
return null;
}
Integer playerNumber = getPositionNumber(parts[6]);
List<String> arenaPlayerNames = new ArrayList<>();
for (K arena : arenas) {
arenaPlayerNames.addAll(getArenaPlayersSorted(arena, gameMode));
}
arenaPlayerNames.sort(Comparator.naturalOrder());
if (playerNumber != null) {
if (playerNumber >= arenaPlayerNames.size()) {
return "";
} else {
return arenaPlayerNames.get(playerNumber);
}
}
}
return null;
}
/**
* Gets information about an arena's players
*
* @param arenaName <p>The name of the arena</p>
* @param gameMode <p>The game-mode to get information for</p>
* @param infoType <p>The type of information to get</p>
* @param parts <p>The placeholder arguments specified by a user</p>
* @return <p>The specified arena info, or null if the placeholder is invalid</p>
*/
@Nullable
private String getArenaInfo(@NotNull String arenaName, @Nullable ArenaGameMode gameMode,
@NotNull PlayerInfoType infoType, @NotNull String[] parts) {
if (infoType == PlayerInfoType.COUNT) {
Set<UUID> arenaPlayers = getArenaPlayers(arenaName, gameMode);
if (arenaPlayers != null) {
return String.valueOf(arenaPlayers.size());
}
} else if (infoType == PlayerInfoType.PLAYER) {
if (parts.length < 7) {
return null;
}
Integer playerNumber = getPositionNumber(parts[6]);
List<String> players = getArenaPlayersSorted(arenaName, gameMode);
if (playerNumber != null && players != null) {
if (playerNumber >= players.size()) {
return "";
} else {
return players.get(playerNumber);
}
}
}
return null;
}
/**
* Gets the position number from the given string
*
* @param positionNumber <p>The position number to parse</p>
* @return <p>The position number, or null if not valid</p>
*/
@Nullable
private Integer getPositionNumber(@NotNull String positionNumber) {
try {
return Integer.parseInt(positionNumber) - 1;
} catch (NumberFormatException exception) {
MiniGames.log(Level.WARNING, "Invalid placeholder given. " + positionNumber +
" supplied instead of player number.");
return null;
}
}
/**
* Gets names of all players in an arena in sorted order
*
* @param arenaName <p>The name of the arena to get players from</p>
* @param arenaGameMode <p>The game-mode to get players for</p>
* @return <p>Player names in sorted order, or null if the arena name is invalid</p>
*/
@Nullable
private List<String> getArenaPlayersSorted(@NotNull String arenaName, @Nullable ArenaGameMode arenaGameMode) {
K arena = arenaHandler.getArena(arenaName);
if (arena == null) {
return null;
}
return getArenaPlayersSorted(arena, arenaGameMode);
}
/**
* Gets names of all players in an arena in sorted order
*
* @param arena <p>The arena to get players from</p>
* @param arenaGameMode <p>The game-mode to get players for</p>
* @return <p>Player names in sorted order, or null if the arena name is invalid</p>
*/
@NotNull
private List<String> getArenaPlayersSorted(@NotNull K arena, @Nullable ArenaGameMode arenaGameMode) {
Set<UUID> players = getArenaPlayers(arena, arenaGameMode);
List<String> playerNames = new ArrayList<>(players.size());
for (UUID playerId : players) {
Player player = Bukkit.getPlayer(playerId);
if (player != null) {
playerNames.add(player.getName());
}
}
playerNames.sort(Comparator.naturalOrder());
return playerNames;
}
/**
* Gets all players from the given arena
*
* @param arenaName <p>The name of the arena to get players from</p>
* @param arenaGameMode <p>The game-mode to get players for</p>
* @return <p>The players in the given arena playing the given game-mode</p>
*/
@Nullable
private Set<UUID> getArenaPlayers(@NotNull String arenaName, @Nullable ArenaGameMode arenaGameMode) {
K arena = arenaHandler.getArena(arenaName);
if (arena == null) {
return null;
}
return getArenaPlayers(arena, arenaGameMode);
}
/**
* Gets all players from the given arena
*
* @param arena <p>The arena to get players from</p>
* @param arenaGameMode <p>The game-mode to get players for</p>
* @return <p>The players in the given arena playing the given game-mode</p>
*/
@NotNull
private Set<UUID> getArenaPlayers(@NotNull K arena, @Nullable ArenaGameMode arenaGameMode) {
// If getting count for any game-mode, skip filtering
Set<UUID> players = playerRegistry.getPlayingPlayers(arena);
if (arenaGameMode == null) {
return players;
}
Set<UUID> output = new HashSet<>();
for (UUID playerId : players) {
ArenaSession arenaSession = playerRegistry.getArenaSession(playerId);
if (arenaSession == null || arenaSession.getGameMode() != arenaGameMode) {
continue;
}
output.add(playerId);
}
return output;
}
}

View File

@ -1,18 +1,16 @@
package net.knarcraft.minigames.placeholder;
package net.knarcraft.minigames.placeholder.parsing;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaGroup;
import net.knarcraft.minigames.arena.ArenaHandler;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.record.ArenaRecord;
import net.knarcraft.minigames.placeholder.parsing.InfoType;
import net.knarcraft.minigames.placeholder.parsing.SelectionType;
import net.knarcraft.minigames.placeholder.GroupRecordCache;
import net.knarcraft.minigames.property.RecordType;
import net.knarcraft.minigames.util.GroupRecordHelper;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -22,60 +20,61 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
/**
* A placeholder expansion for parkour record placeholders
* A parser for record-related placeholders
*/
public abstract class RecordExpansion extends PlaceholderExpansion {
public class RecordPlaceholderParser {
private final ArenaHandler<?, ?> arenaHandler;
private final Map<UUID, Set<GroupRecordCache<Integer>>> groupRecordDeathsCache;
private final Map<UUID, Set<GroupRecordCache<Long>>> groupRecordTimeCache;
private final @NotNull ArenaHandler<?, ?> arenaHandler;
private final @NotNull Map<UUID, Set<GroupRecordCache<Integer>>> groupRecordDeathsCache;
private final @NotNull Map<UUID, Set<GroupRecordCache<Long>>> groupRecordTimeCache;
private final @NotNull Function<String, ArenaGameMode> gameModeParser;
/**
* Initializes a new record expansion
* Initializes a new record placeholder parser
*/
public RecordExpansion(ArenaHandler<?, ?> arenaHandler) {
public RecordPlaceholderParser(@NotNull ArenaHandler<?, ?> arenaHandler,
@NotNull Function<String, ArenaGameMode> gameModeParser) {
this.groupRecordDeathsCache = new HashMap<>();
this.groupRecordTimeCache = new HashMap<>();
this.arenaHandler = arenaHandler;
this.gameModeParser = gameModeParser;
}
@Override
public String getAuthor() {
return "EpicKnarvik97";
/**
* The method to run when parsing a record placeholder request
*
* @param parts <p>All parts of the placeholder</p>
* @return <p>The resulting string</p>
*/
@Nullable
public String onRequest(@NotNull String[] parts) {
if (parts.length < 7) {
return null;
}
@Override
public String getVersion() {
return "1.0.0";
}
@Override
public boolean persist() {
return true;
}
@Override
public String onRequest(OfflinePlayer player, String parameters) {
String[] parts = parameters.split("_");
// Record is used as the prefix for all record placeholders in case more placeholder types are added
if (parts.length < 7 || !parts[0].equals("record")) {
return parameters;
}
RecordType recordType = RecordType.getFromString(parts[1]);
ArenaGameMode gameMode = parseGameMode(parts[2]);
ArenaGameMode gameMode = gameModeParser.apply(parts[2]);
SelectionType selectionType = SelectionType.getFromString(parts[3]);
String identifier = parts[4];
int recordNumber = Integer.parseInt(parts[5]) - 1;
int recordNumber;
try {
recordNumber = Integer.parseInt(parts[5]) - 1;
} catch (NumberFormatException exception) {
MiniGames.log(Level.WARNING, "Invalid placeholder given. " + parts[5] +
" supplied instead of record position.");
return null;
}
InfoType infoType = InfoType.getFromString(parts[6]);
if (recordType == null || infoType == null) {
return parameters;
return null;
}
String info = null;
@ -85,17 +84,9 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
info = getArenaRecord(arenaHandler, identifier, gameMode, recordType, recordNumber, infoType);
}
return Objects.requireNonNullElse(info, parameters);
return info;
}
/**
* Parses the game-mode specified in the given string
*
* @param gameMode <p>The game-mode to parse</p>
* @return <p>The parsed game-mode</p>
*/
protected abstract @NotNull ArenaGameMode parseGameMode(@NotNull String gameMode);
/**
* Clears all record caches
*/
@ -111,11 +102,12 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param identifier <p>The identifier (name/uuid) selecting the group</p>
* @param gameMode <p>The game-mode to get a record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param recordNumber <p>The position of the record to get (1st place, 2nd place, etc.)</p>
* @param infoType <p>The type of info (player, value, combined) to get</p>
* @return <p>The selected information about the record, or null if not found</p>
*/
private @Nullable String getGroupRecord(@NotNull ArenaHandler<?, ?> arenaHandler, @NotNull String identifier,
@Nullable
private String getGroupRecord(@NotNull ArenaHandler<?, ?> arenaHandler, @NotNull String identifier,
@NotNull ArenaGameMode gameMode, @NotNull RecordType recordType,
int recordNumber, @NotNull InfoType infoType) {
// Allow specifying the group UUID or the arena name
@ -149,11 +141,12 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
*
* @param group <p>The group to get the record from</p>
* @param gameMode <p>The game-mode to get the record from</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param recordNumber <p>The position of the record to get (1st place, 2nd place, etc.)</p>
* @param arenaHandler <p>The handler to get arenas from</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getGroupTimeRecord(@NotNull ArenaGroup<?, ?> group,
@Nullable
private ArenaRecord<?> getGroupTimeRecord(@NotNull ArenaGroup<?, ?> group,
@NotNull ArenaGameMode gameMode, int recordNumber,
@NotNull ArenaHandler<?, ?> arenaHandler) {
return getCachedGroupRecord(group, gameMode, RecordType.TIME, recordNumber, groupRecordTimeCache,
@ -165,11 +158,12 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
*
* @param group <p>The group to get the record from</p>
* @param gameMode <p>The game-mode to get the record from</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param recordNumber <p>The position of the record to get (1st place, 2nd place, etc.)</p>
* @param arenaHandler <p>The handler to get arenas from</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getGroupDeathRecord(@NotNull ArenaGroup<?, ?> group,
@Nullable
private ArenaRecord<?> getGroupDeathRecord(@NotNull ArenaGroup<?, ?> group,
@NotNull ArenaGameMode gameMode, int recordNumber,
@NotNull ArenaHandler<?, ?> arenaHandler) {
return getCachedGroupRecord(group, gameMode, RecordType.DEATHS, recordNumber, groupRecordDeathsCache,
@ -182,13 +176,14 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param group <p>The group to get the record for</p>
* @param gameMode <p>The game-mode to get the record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param recordNumber <p>The position of the record to get (1st place, 2nd place, etc.)</p>
* @param caches <p>The caches to use for looking for and saving the record</p>
* @param recordProvider <p>The provider of records if the cache cannot provide the record</p>
* @param <K> <p>The type of the provided records</p>
* @return <p>The specified record, or null if not found</p>
*/
private <K extends Comparable<K>> @Nullable ArenaRecord<?> getCachedGroupRecord(@NotNull ArenaGroup<?, ?> group,
@Nullable
private <K extends Comparable<K>> ArenaRecord<?> getCachedGroupRecord(@NotNull ArenaGroup<?, ?> group,
@NotNull ArenaGameMode gameMode,
@NotNull RecordType recordType,
int recordNumber,
@ -231,11 +226,12 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param identifier <p>The identifier (name/uuid) selecting the arena</p>
* @param gameMode <p>The game-mode to get a record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param recordNumber <p>The position of the record to get (1st place, 2nd place, etc.)</p>
* @param infoType <p>The type of info (player, value, combined) to get</p>
* @return <p>The selected information about the record, or null if not found</p>
*/
private @Nullable String getArenaRecord(@NotNull ArenaHandler<?, ?> arenaHandler, @NotNull String identifier,
@Nullable
private String getArenaRecord(@NotNull ArenaHandler<?, ?> arenaHandler, @NotNull String identifier,
@NotNull ArenaGameMode gameMode, @NotNull RecordType recordType,
int recordNumber, @NotNull InfoType infoType) {
// Allow specifying the arena UUID or the arena name
@ -266,10 +262,11 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
*
* @param recordsRegistry <p>The records registry to get the record from</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param recordNumber <p>The position of the record to get (1st place, 2nd place, etc.)</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getRecord(@NotNull ArenaRecordsRegistry recordsRegistry,
@Nullable
private ArenaRecord<?> getRecord(@NotNull ArenaRecordsRegistry recordsRegistry,
@NotNull RecordType recordType, int recordNumber) {
return switch (recordType) {
case TIME -> getRecord(new HashSet<>(recordsRegistry.getShortestTimeMilliSecondsRecords()), recordNumber);
@ -285,7 +282,8 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param <K> <p>The type of record in the record list</p>
* @return <p>The record, or null if index is out of bounds</p>
*/
private <K extends Comparable<K>> @Nullable ArenaRecord<K> getRecord(Set<ArenaRecord<K>> records, int index) {
@Nullable
private <K extends Comparable<K>> ArenaRecord<K> getRecord(@NotNull Set<ArenaRecord<K>> records, int index) {
List<ArenaRecord<K>> sorted = getSortedRecords(records);
if (index < sorted.size() && index >= 0) {
return sorted.get(index);
@ -301,11 +299,12 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param arenaRecord <p>The record to get the data from</p>
* @return <p>The requested data as a string, or null</p>
*/
@Nullable
private String getRecordData(@NotNull InfoType infoType, @NotNull ArenaRecord<?> arenaRecord) {
return switch (infoType) {
case PLAYER -> getPlayerName(arenaRecord.getUserId());
case VALUE -> arenaRecord.getRecord().toString();
case COMBINED -> getPlayerName(arenaRecord.getUserId()) + ": " + arenaRecord.getRecord().toString();
case VALUE -> arenaRecord.getAsString();
case COMBINED -> getPlayerName(arenaRecord.getUserId()) + ": " + arenaRecord.getAsString();
};
}
@ -316,8 +315,8 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param <K> <p>The type of the records</p>
* @return <p>The sorted records</p>
*/
private <K extends Comparable<K>> @NotNull List<ArenaRecord<K>> getSortedRecords(
@NotNull Set<ArenaRecord<K>> recordSet) {
@NotNull
private <K extends Comparable<K>> List<ArenaRecord<K>> getSortedRecords(@NotNull Set<ArenaRecord<K>> recordSet) {
List<ArenaRecord<K>> records = new ArrayList<>(recordSet);
Collections.sort(records);
return records;
@ -329,6 +328,7 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
* @param playerId <p>The id of the player to get the name for</p>
* @return <p>The name of the player, or a string representation of the UUID if not found</p>
*/
@Nullable
private String getPlayerName(@NotNull UUID playerId) {
return Bukkit.getOfflinePlayer(playerId).getName();
}

View File

@ -0,0 +1,45 @@
package net.knarcraft.minigames.property;
/**
* An enum for all persistent data keys used by this plugin
*/
public enum PersistentDataKey {
MENU_ITEM("MiniGamesMenu", 1799804),
LEAVE_ITEM("MiniGamesAction", 1799871),
GIVE_UP_ITEM("MiniGamesAction", 1799872),
;
private final String keyName;
private final int dataValue;
/**
* Instantiates a new persistent data key
*
* @param keyName <p>The name of this key</p>
* @param dataValue <p>The integer data value of this key</p>
*/
PersistentDataKey(String keyName, int dataValue) {
this.keyName = keyName;
this.dataValue = dataValue;
}
/**
* Gets the name of this persistent data key
*
* @return <p>The name of this key</p>
*/
public String getKeyName() {
return this.keyName;
}
/**
* Gets the integer data value of this persistent data key
*
* @return <p>The integer data value</p>
*/
public int getDataValue() {
return this.dataValue;
}
}

View File

@ -1,18 +1,72 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.PlayerEntryState;
import net.knarcraft.minigames.arena.StorageKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* A helper class for dealing with arena storage
*/
public final class ArenaStorageHelper {
private ArenaStorageHelper() {
}
/**
* Stores the given entry states to disk
*
* @param key <p>The key specifying the correct entry state file</p>
* @param entryStates <p>The entry states to save</p>
*/
public static void storeArenaPlayerEntryStates(String key, Set<PlayerEntryState> entryStates) {
YamlConfiguration configuration = new YamlConfiguration();
configuration.set(key, new ArrayList<>(entryStates));
try {
configuration.save(new File(MiniGames.getInstance().getDataFolder(), key + "EntryStates.yml"));
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save entry states to disk");
}
}
/**
* Gets saved entry states from disk
*
* @param key <p>The key specifying the correct entry state file</p>
* @return <p>The previously saved entry states</p>
*/
public static Set<PlayerEntryState> getArenaPlayerEntryStates(String key) {
File entryStateFile = new File(MiniGames.getInstance().getDataFolder(), key + "EntryStates.yml");
if (!entryStateFile.exists()) {
return new HashSet<>();
}
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(entryStateFile);
Set<PlayerEntryState> output = new HashSet<>();
List<?> entries = configuration.getList(key, new ArrayList<>());
for (Object entry : entries) {
if (entry instanceof PlayerEntryState entryState) {
output.add(entryState);
}
}
return output;
}
/**
* Gets the file used to store the given arena id's data
*
@ -20,7 +74,7 @@ public final class ArenaStorageHelper {
* @param arenaId <p>The id of the arena to get a data file for</p>
* @return <p>The file the arena's data is/should be stored in</p>
*/
static @NotNull File getArenaDataFile(File root, @NotNull UUID arenaId) {
public static @NotNull File getArenaDataFile(File root, @NotNull UUID arenaId) {
File arenaDataFile = new File(root, arenaId + ".yml");
if (!root.exists() && !root.mkdirs()) {
MiniGames.log(Level.SEVERE, "Unable to create the arena data directories");
@ -28,4 +82,40 @@ public final class ArenaStorageHelper {
return arenaDataFile;
}
/**
* Loads a set of strings from the given configuration section
*
* @param configurationSection <p>The configuration section to load from</p>
* @param storageKey <p>The key to the info to load</p>
* @return <p>The loaded items, or null if not set</p>
*/
@Nullable
@SuppressWarnings("unchecked")
public static Set<String> loadStrings(@NotNull ConfigurationSection configurationSection,
@NotNull StorageKey storageKey) {
List<?> rawList = configurationSection.getList(storageKey.getKey());
Set<String> output;
if (rawList == null) {
output = null;
} else {
output = new HashSet<>((List<String>) rawList);
}
return output;
}
/**
* Gets the names of the given damage causes
*
* @param causes <p>The damage causes to get names of</p>
* @return <p>The names of the damage causes</p>
*/
@NotNull
public static List<String> getDamageCauseNames(@NotNull Set<EntityDamageEvent.DamageCause> causes) {
List<String> output = new ArrayList<>(causes.size());
for (EntityDamageEvent.DamageCause cause : causes) {
output.add(cause.name());
}
return output;
}
}

View File

@ -9,6 +9,9 @@ import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaStorageKey;
import net.knarcraft.minigames.arena.reward.Reward;
import net.knarcraft.minigames.arena.reward.RewardCondition;
import net.knarcraft.minigames.config.MiniGameMessage;
import net.knarcraft.minigames.container.SerializableMaterial;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.Location;
@ -17,6 +20,7 @@ import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.error.YAMLException;
import java.io.File;
import java.io.IOException;
@ -28,6 +32,7 @@ import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.minigames.util.ArenaStorageHelper.getArenaDataFile;
import static net.knarcraft.minigames.util.ArenaStorageHelper.loadStrings;
/**
* A helper class for saving and loading arenas
@ -94,6 +99,35 @@ public final class DropperArenaStorageHelper {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(dropperArenasConfigurationSection);
for (DropperArena arena : arenas.values()) {
saveDropperArena(arenaSection, arena);
}
configuration.save(dropperArenaFile);
}
/**
* Saves a single arena
*
* @param arena <p>The arena to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveSingleDropperArena(DropperArena arena) throws IOException {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(dropperArenaFile);
ConfigurationSection arenaSection = configuration.getConfigurationSection(dropperArenasConfigurationSection);
if (arenaSection == null) {
arenaSection = configuration.createSection(dropperArenasConfigurationSection);
}
saveDropperArena(arenaSection, arena);
configuration.save(dropperArenaFile);
}
/**
* Updates the given configuration section with the arena's data, and stores arena data for the arena
*
* @param arenaSection <p>The configuration section to update</p>
* @param arena <p>The arena to save</p>
* @throws IOException <p>If unable to save the arena data</p>
*/
private static void saveDropperArena(ConfigurationSection arenaSection, DropperArena arena) throws IOException {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
@ -104,10 +138,14 @@ public final class DropperArenaStorageHelper {
configSection.set(DropperArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey(), arena.getPlayerVerticalVelocity());
configSection.set(DropperArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey(), arena.getPlayerHorizontalVelocity());
configSection.set(DropperArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
configSection.set(DropperArenaStorageKey.MAX_PLAYERS.getKey(), arena.getMaxPlayers());
configSection.set(DropperArenaStorageKey.ALLOWED_DAMAGE_CAUSES.getKey(),
ArenaStorageHelper.getDamageCauseNames(arena.getAllowedDamageCauses()));
configSection.set(DropperArenaStorageKey.LOSS_TRIGGER_DAMAGE_CAUSES.getKey(),
ArenaStorageHelper.getDamageCauseNames(arena.getLossTriggerDamageCauses()));
RewardStorageHelper.saveRewards(arena, configSection, DropperArenaStorageKey.REWARDS.getKey());
saveDropperArenaData(arena.getData());
}
configuration.save(dropperArenaFile);
}
/**
* Loads all arenas
@ -155,27 +193,37 @@ public final class DropperArenaStorageHelper {
double verticalVelocity = configurationSection.getDouble(DropperArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey());
float horizontalVelocity = sanitizeHorizontalVelocity((float) configurationSection.getDouble(
DropperArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey()));
int maxPlayers = configurationSection.getInt(DropperArenaStorageKey.MAX_PLAYERS.getKey(), -1);
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
DropperArenaStorageKey.WIN_BLOCK_TYPE.getKey());
if (arenaName == null || spawnLocation == null) {
MiniGames.log(Level.SEVERE, "Could not load the arena at configuration " +
"section " + configurationSection.getName() + ". Please check the dropper_arenas storage file for issues.");
MiniGames.log(Level.SEVERE, MiniGames.getInstance().getStringFormatter().replacePlaceholders(
MiniGameMessage.ERROR_ARENA_NOT_LOADED, new String[]{"{section}", "{file}"},
new String[]{configurationSection.getName(), "dropper_arenas"}));
return null;
}
if (winBlockType == null) {
winBlockType = new SerializableMaterial(Material.WATER);
}
Map<RewardCondition, Set<Reward>> rewards = RewardStorageHelper.loadRewards(configurationSection,
DropperArenaStorageKey.REWARDS.getKey());
// Generate new, empty arena data if not available
DropperArenaData arenaData = loadDropperArenaData(arenaId);
if (arenaData == null) {
MiniGames.log(Level.SEVERE, "Unable to load arena data for dropper arena" + arenaId);
MiniGames.log(Level.SEVERE, MiniGames.getInstance().getStringFormatter().replacePlaceholder(
MiniGameMessage.ERROR_ARENA_DATA_NOT_LOADED, "{arena}", arenaId.toString()));
arenaData = getEmptyDropperData(arenaId);
}
Set<String> allowedDamageCauseNames = loadStrings(configurationSection, DropperArenaStorageKey.ALLOWED_DAMAGE_CAUSES);
Set<String> lossTriggerDamageCauseNames = loadStrings(configurationSection, DropperArenaStorageKey.LOSS_TRIGGER_DAMAGE_CAUSES);
return new DropperArena(arenaId, arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity,
winBlockType.getRawValue(), arenaData, MiniGames.getInstance().getDropperArenaHandler());
winBlockType.getRawValue(), maxPlayers, rewards, arenaData,
MiniGames.getInstance().getDropperArenaHandler(), allowedDamageCauseNames, lossTriggerDamageCauseNames);
}
/**
@ -213,9 +261,14 @@ public final class DropperArenaStorageHelper {
* @return <p>The loaded arena data</p>
*/
private static @Nullable DropperArenaData loadDropperArenaData(@NotNull UUID arenaId) {
try {
File arenaDataFile = getDropperArenaDataFile(arenaId);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaDataFile);
return (DropperArenaData) configuration.get(DropperArenaStorageKey.DATA.getKey());
} catch (YAMLException exception) {
MiniGames.log(Level.SEVERE, "Unable to load arena data from arena " + arenaId);
return null;
}
}
/**

View File

@ -0,0 +1,95 @@
package net.knarcraft.minigames.util;
import net.knarcraft.knargui.item.GUIItemFactory;
import net.knarcraft.knargui.item.PlayerHeadGUIItemFactory;
import net.knarcraft.knargui.item.SimpleGUIItemFactory;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.gui.MiniGamesGUI;
import net.knarcraft.minigames.property.PersistentDataKey;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
/**
* A helper class for the in-arena GUI
*/
public final class GUIHelper {
private GUIHelper() {
}
/**
* Gets the GUI open item to give to the specified player
*
* @param player <p>The player to give a GUI opening item</p>
* @return <p>The item to give</p>
*/
public static ItemStack getGUIOpenItem(@NotNull Player player) {
if (GeyserHelper.isGeyserPlayer(player)) {
return getGUIOpenItemBedrock();
} else {
return getGUIOpenItemJava();
}
}
/**
* Gets the item used for opening the mini-games menu
*
* @return <p>The item used for opening the GUI</p>
*/
public static ItemStack getGUIOpenItemJava() {
PlayerHeadGUIItemFactory factory = new PlayerHeadGUIItemFactory();
factory.useSkin("3fdab40434ed5d01f58c45ca0c9fada4662e1772ff43e2974979440a5cfe15c9");
factory.setName(ChatColor.AQUA + "§ MiniGames Menu §");
ItemStack item = factory.build();
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.getPersistentDataContainer().set(new NamespacedKey(MiniGames.getInstance(),
PersistentDataKey.MENU_ITEM.getKeyName()),
PersistentDataType.INTEGER, PersistentDataKey.MENU_ITEM.getDataValue());
}
item.setItemMeta(meta);
return item;
}
/**
* Gets the item used for opening the mini-games menu
*
* @return <p>The item used for opening the GUI</p>
*/
public static ItemStack getGUIOpenItemBedrock() {
GUIItemFactory factory = new SimpleGUIItemFactory(Material.BEACON);
factory.setName(ChatColor.AQUA + "MiniGames Menu");
ItemStack item = factory.build();
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.getPersistentDataContainer().set(new NamespacedKey(MiniGames.getInstance(),
PersistentDataKey.MENU_ITEM.getKeyName()),
PersistentDataType.INTEGER, PersistentDataKey.MENU_ITEM.getDataValue());
}
item.setItemMeta(meta);
return item;
}
/**
* Opens the correct GUI for the given player
*
* @param player <p>The player to show a GUI for</p>
*/
public static void openGUI(Player player) {
ArenaSession existingSession = MiniGames.getInstance().getSession(player.getUniqueId());
if (existingSession == null) {
new MiniGamesGUI(player).openFor(player);
} else {
existingSession.getGUI().openFor(player);
}
}
}

View File

@ -0,0 +1,53 @@
package net.knarcraft.minigames.util;
import org.bukkit.entity.Player;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.geyser.api.GeyserApi;
import org.jetbrains.annotations.NotNull;
/**
* A helper class for dealing with geyser/floodgate players
*/
public final class GeyserHelper {
private static boolean hasGeyser = true;
private static boolean hasFloodgate = true;
private GeyserHelper() {
}
/**
* Checks whether the given player is connected through Geyser
*
* @param player <p>The player to check</p>
* @return <p>True if the player is connected through Geyser</p>
*/
public static boolean isGeyserPlayer(@NotNull Player player) {
// Prevent unnecessary checking for non-geyser and floodgate servers
if (!hasGeyser && !hasFloodgate) {
return false;
}
// Use Geyser API to get connection status
if (hasGeyser) {
try {
return GeyserApi.api().connectionByUuid(player.getUniqueId()) != null;
} catch (NoClassDefFoundError error1) {
hasGeyser = false;
}
}
// Use Floodgate API to get connection status
if (hasFloodgate) {
try {
return FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId());
} catch (NoClassDefFoundError error2) {
hasFloodgate = false;
}
}
return false;
}
}

View File

@ -1,7 +1,15 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
/**
* A helper class for validating whether given input is valid
@ -18,9 +26,48 @@ public final class InputValidationHelper {
* @param location <p>The location to validate</p>
* @return <p>False if the location is valid</p>
*/
public static boolean isInvalid(Location location) {
public static boolean isInvalid(@Nullable Location location) {
if (location == null) {
return true;
}
World world = location.getWorld();
return world == null || !world.getWorldBorder().isInside(location);
}
/**
* Checks whether the given value can be considered "empty"
*
* @param value <p>The value to check</p>
* @return <p>True if the value can be considered as empty</p>
*/
public static boolean isEmptyValue(@NotNull String value) {
return value.equalsIgnoreCase("null") || value.equalsIgnoreCase("clear") ||
value.equalsIgnoreCase("none");
}
/**
* Parses a set of damage causes from a set of damage cause names
*
* @param input <p>The damage cause names to parse</p>
* @return <p>The resulting damage causes</p>
*/
@NotNull
public static Set<EntityDamageEvent.DamageCause> parseDamageCauses(@Nullable Set<String> input) {
Set<EntityDamageEvent.DamageCause> output = new HashSet<>();
if (input == null) {
return output;
}
for (String causeName : input) {
try {
output.add(EntityDamageEvent.DamageCause.valueOf(causeName));
} catch (IllegalArgumentException | NullPointerException exception) {
MiniGames.log(Level.WARNING, "The damage cause " + causeName +
" is invalid, and will be ignored.");
}
}
return output;
}
}

Some files were not shown because too many files have changed in this diff Show More